mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 11:40:52 +01:00
Compare commits
70 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6018f2fb12 | ||
|
|
52c69fc293 | ||
|
|
4dc17f3aca | ||
|
|
d22280f177 | ||
|
|
9e8f7092ea | ||
|
|
de188cc773 | ||
|
|
784a02c0e7 | ||
|
|
c8db7f98db | ||
|
|
318c37e686 | ||
|
|
524be86449 | ||
|
|
ddeb78accd | ||
|
|
585e847818 | ||
|
|
ac3cb4dc2a | ||
|
|
c06ca39009 | ||
|
|
06b11a51f1 | ||
|
|
4938a159d4 | ||
|
|
27f2e270ce | ||
|
|
94e60e0b10 | ||
|
|
c6863033a5 | ||
|
|
917878d05f | ||
|
|
10e4766809 | ||
|
|
abd5149245 | ||
|
|
912b6da20a | ||
|
|
94fe0b7721 | ||
|
|
9fac1e78fb | ||
|
|
6697b76683 | ||
|
|
b2ab247b5b | ||
|
|
7411ae8138 | ||
|
|
d398e3a75b | ||
|
|
a5047224bb | ||
|
|
058821c08b | ||
|
|
0c691b061d | ||
|
|
51bbc5dc45 | ||
|
|
2d3522e752 | ||
|
|
d4f265c314 | ||
|
|
7654d38612 | ||
|
|
cdcc255123 | ||
|
|
2f56a6a110 | ||
|
|
16fc48cef2 | ||
|
|
5cecbb2cff | ||
|
|
6115d6d5cc | ||
|
|
60d26be139 | ||
|
|
186392ea80 | ||
|
|
ebe4538d4c | ||
|
|
745d0e5532 | ||
|
|
d4f7e60432 | ||
|
|
ced127c164 | ||
|
|
f91bcb62b1 | ||
|
|
5268a4a3f0 | ||
|
|
1f1e50539b | ||
|
|
ea3132bbba | ||
|
|
67ccaec418 | ||
|
|
40b70e9447 | ||
|
|
7d37db9ce0 | ||
|
|
dd8688df3d | ||
|
|
b02c53c6ad | ||
|
|
38d3b83818 | ||
|
|
f02cd0083a | ||
|
|
c2c8af16d0 | ||
|
|
b61003e2a0 | ||
|
|
fb0ec52f8c | ||
|
|
fee79d8aa5 | ||
|
|
4508105412 | ||
|
|
a0ebb290e2 | ||
|
|
d45497e53b | ||
|
|
ff8dd021c3 | ||
|
|
34a371ef1f | ||
|
|
83109b08e9 | ||
|
|
3d7b83db05 | ||
|
|
41b2ee19a1 |
@@ -59,6 +59,7 @@
|
||||
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageVersion Include="SpaceWizards.HttpListener" Version="0.1.1" />
|
||||
<PackageVersion Include="SpaceWizards.NFluidsynth" Version="0.2.2" />
|
||||
<PackageVersion Include="SpaceWizards.Sdl" Version="1.0.0" />
|
||||
<PackageVersion Include="SpaceWizards.SharpFont" Version="1.1.0" />
|
||||
<PackageVersion Include="SpaceWizards.Sodium" Version="0.2.1" />
|
||||
<PackageVersion Include="libsodium" Version="1.0.20.1" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
126
RELEASE-NOTES.md
126
RELEASE-NOTES.md
@@ -54,12 +54,134 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 267.0.2
|
||||
## 267.2.2
|
||||
|
||||
|
||||
## 267.0.1
|
||||
## 267.2.1
|
||||
|
||||
|
||||
## 267.2.0
|
||||
|
||||
### New features
|
||||
|
||||
* Sprites and Sprite layers have a new `Loop` data field that can be set to false to automatically pause animations once they have finished.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed `CollectionExtensions.TryGetValue` throwing an exception when given a negative list index.
|
||||
* Fixed `EntityManager.PredictedQueueDeleteEntity()` not deferring changes for networked entities until the end of the tick.
|
||||
* Fixed `EntityManager.IsQueuedForDeletion` not returning true foe entities getting deleted via `PredictedQueueDeleteEntity()`
|
||||
|
||||
### Other
|
||||
|
||||
* `IResourceManager.GetContentRoots()` has been obsoleted and returns no more results.
|
||||
|
||||
### Internal
|
||||
|
||||
* `IResourceManager.GetContentRoots()` has been replaced with a similar method on `IResourceManagerInternal`. This new method returns `string`s instead of `ResPath`s, and usage code has been updated to use these paths correctly.
|
||||
|
||||
|
||||
## 267.1.0
|
||||
|
||||
### New features
|
||||
|
||||
* Animation:
|
||||
* `AnimationTrackProperty.KeyFrame` can now have easings functions applied.
|
||||
* Graphics:
|
||||
* `PointLightComponent` now has two fields, `falloff` and `curveFactor`, for controlling light falloff and the shape of the light attenuation curve.
|
||||
* `IClydeViewport` now has an `Id` and `ClearCachedResources` event. Together, these allow you to properly cache rendering resources per viewport.
|
||||
* Miscellaneous:
|
||||
* Added `display.max_fps` CVar.
|
||||
* Added `IGameTiming.FrameStartTime`.
|
||||
* Sandbox:
|
||||
* Added `System.WeakReference<T>`.
|
||||
* Added `SpaceWizards.Sodium.CryptoGenericHashBlake2B.Hash()`.
|
||||
* Added `System.Globalization.UnicodeCategory`.
|
||||
* Serialization:
|
||||
* Added a new entity yaml deserialization option (`SerializationOptions.EntityExceptionBehaviour`) that can optionally make deserialization more exception tolerant.
|
||||
* Tooling:
|
||||
* `devwindow` now has a tab listing active `IRenderTarget`s, allowing insight into resource consumption.
|
||||
* `loadgrid` now creates a map if passed an invalid map ID.
|
||||
* Added game version information to F3 overlay.
|
||||
* Added completions to more map commands.
|
||||
* UI system:
|
||||
* `Control.OrderedChildCollection` (gotten from `.Children`) now implements `IReadOnlyList<Control>`, allowing it to be indexed directly.
|
||||
* Added `WrapContainer` control. This lays out multiple elements along an axis, wrapping them if there's not enough space. It comes with many options and can handle multiple axes.
|
||||
* Popups/modals now work in secondary windows. This entails putting roots for these on each UI root.
|
||||
* If you are not using `OSWindow` and are instead creating secondary windows manually, you need to call `WindowRoot.CreateRootControls()` manually for this to work.
|
||||
* Added `Axis` enum, `IAxisImplementation` interface and axis implementations. These allow writing general-purpose UI layout code that can work on multiple axis at once.
|
||||
* WebView:
|
||||
* Added `web.remote_debug_port` CVar to change Chromium's remote debug port.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Audio:
|
||||
* Fix audio occlusion & velocity being calculated with the audio entity instead of the source entity.
|
||||
* Bound UI:
|
||||
* Try to fix an assert related to `UserInterfaceComponent` delta states.
|
||||
* Configuration:
|
||||
* The client no longer tries to send `CLIENT | REPLICATED` CVars when not connected to a server. This could cause test failures.
|
||||
* Math:
|
||||
* Fixed `Matrix3Helpers.TransformBounds()` returning an incorrect result. Now it effectively behaves like `Matrix3Helpers.TransformBox()` and has been marked as obsolete.
|
||||
* Physics:
|
||||
* Work around an undiagnosed crash processing entities without parents.
|
||||
* Serialization:
|
||||
* Fix `[DataRecord]`s with computed get-only properties.
|
||||
* Resources:
|
||||
* Fix some edge case broken path joining in `DirLoader` and `WritableDirProvider`.
|
||||
* Tests:
|
||||
* Fix `PlacementManager.CurrentMousePosition` in integration tests.
|
||||
* UI system:
|
||||
* Animations for the debug console and scrolling are no longer framerate dependent.
|
||||
* Fix `OutputPanel.SetMessage` triggering a scrolling animation when editing messages other than the last one.
|
||||
* Fix word wrapping with two-`char` runes in `RichTextLabel` and `OutputPanel`.
|
||||
* WebView:
|
||||
* Multiple clients with WebView can now run at the same time, thanks to better CEF cache management.
|
||||
|
||||
### Other
|
||||
|
||||
* Audio:
|
||||
* Improved error logging for invalid file names in `SharedAudioSystem`.
|
||||
* Configuration:
|
||||
* Fix crash if more than 255 `REPLICATED` CVars exist. Also increased the max size of the CVar replication message.
|
||||
* Entities:
|
||||
* Transform:
|
||||
* `AnchorEntity` logs instead of using an assert for invalid arguments.
|
||||
* Containers:
|
||||
* `SharedContainerSystem.CleanContainer` now uses `PredictedDel()` instead.
|
||||
* Networking:
|
||||
* The client now logs an error when attempting to send a network message without server connection. Previously, it would be silently dropped.
|
||||
* `net.interp` and `net.buffer_size` CVars are now `REPLICATED`.
|
||||
* Graphics:
|
||||
* The function used for pointlight attenuation has been modified to be c1 continuous as opposed to simply c0 continuous, resulting in smoother boundary behavior.
|
||||
* RSI validator no longer allows empty (`""`) state names.
|
||||
* Packaging:
|
||||
* Server packaging now excludes all files in the `Audio/` directory.
|
||||
* Server packaging now excludes engine resources `EngineFonts/` and `Midi/`.
|
||||
* ACZ explicitly specifies manifest charset as UTF-8.
|
||||
* Serialization:
|
||||
* `CurTime`-relative `TimeSpan` values that are `MaxValue` now deserialize without overflow.
|
||||
* `SpriteSpecifier.Texture` will now fail to validate if the path is inside a `.rsi`. Use RSI sprite specifiers instead.
|
||||
* Resources:
|
||||
* `IWritableDirProvider.RootDir` is now null on clients.
|
||||
* WebView:
|
||||
* CEF cache is no longer in the content-accessible user data directory.
|
||||
|
||||
### Internal
|
||||
|
||||
* Added some debug commands for debugging viewport resource management: `vp_clear_all_cached` & `vp_test_finalize`
|
||||
* `uitest` command now supports command argument for tab selection, like `uitest2`.
|
||||
* Rewrote `BoxContainer` implementation to make use of new axis system.
|
||||
* Moved `uitest2` and `devwindow` to use the `OSWindow` control.
|
||||
* SDL3 binding has been moved to `SpaceWizards.Sdl` NuGet package.
|
||||
* `dmetamem` command has been moved from `DEBUG` to `TOOLS`.
|
||||
* Consolidate `AttachToGridOrMap` with `TryGetMapOrGridCoordinates`.
|
||||
* Secondary window render targets have clear names specified.
|
||||
* Updated `SpaceWizards.NFluidsynth` to `0.2.2`.
|
||||
* `Robust.Client.WebView.Cef.Program` is now internal.
|
||||
* `download_manifest_file.py` script in repo now always decodes as UTF-8 correctly.
|
||||
* Added a new debug assert to game state processing.
|
||||
|
||||
## 267.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
3
Resources/Locale/en-US/_generic.ftl
Normal file
3
Resources/Locale/en-US/_generic.ftl
Normal file
@@ -0,0 +1,3 @@
|
||||
generic-map = map
|
||||
generic-grid = grid
|
||||
generic-mapid = map Id
|
||||
@@ -577,3 +577,5 @@ cmd-localization_set_culture-desc = Set DefaultCulture for the client Localizati
|
||||
cmd-localization_set_culture-help = Usage: localization_set_culture <cultureName>
|
||||
cmd-localization_set_culture-culture-name = <cultureName>
|
||||
cmd-localization_set_culture-changed = Localization changed to { $code } ({ $nativeName } / { $englishName })
|
||||
|
||||
cmd-addmap-hint-2 = runMapInit [true / false]
|
||||
|
||||
@@ -8,3 +8,18 @@ dev-window-tab-textures-info = Width: { $width } Height: { $height }
|
||||
PixelType: { $pixelType } sRGB: { $srgb }
|
||||
Name: { $name }
|
||||
Est. memory usage: { $bytes }
|
||||
|
||||
## "Render Targets" dev window tab
|
||||
dev-window-tab-render-targets-title = Render Targets
|
||||
dev-window-tab-render-targets-reload = Reload
|
||||
dev-window-tab-render-targets-filter = Filter
|
||||
dev-window-tab-render-targets-column-id = ID
|
||||
dev-window-tab-render-targets-column-name = Name
|
||||
dev-window-tab-render-targets-column-size = Size
|
||||
dev-window-tab-render-targets-column-type = Type
|
||||
dev-window-tab-render-targets-column-vram = VRAM
|
||||
dev-window-tab-render-targets-column-thumbnail = Thumbnail
|
||||
|
||||
dev-window-tab-render-targets-value-null = null
|
||||
dev-window-tab-render-targets-value-not-available = Not available
|
||||
dev-window-tab-render-targets-summary = Total VRAM: { $vram }
|
||||
|
||||
@@ -14,6 +14,8 @@ uniform highp vec2 lightCenter;
|
||||
uniform highp float lightRange;
|
||||
uniform highp float lightPower;
|
||||
uniform highp float lightSoftness;
|
||||
uniform highp float lightFalloff;
|
||||
uniform highp float lightCurveFactor;
|
||||
uniform highp float lightIndex;
|
||||
uniform sampler2D shadowMap;
|
||||
|
||||
@@ -47,8 +49,15 @@ void fragment()
|
||||
discard;
|
||||
}
|
||||
|
||||
highp float dist = dot(diff, diff) + LIGHTING_HEIGHT;
|
||||
highp float val = clamp((1.0 - clamp(sqrt(dist) / lightRange, 0.0, 1.0)) * (1.0 / (sqrt(dist + 1.0))), 0.0, 1.0);
|
||||
// this implementation of light attenuation primarily adapted from
|
||||
// https://lisyarus.github.io/blog/posts/point-light-attenuation.html
|
||||
highp float sqr_dist = dot(diff, diff) + LIGHTING_HEIGHT;
|
||||
|
||||
highp float s = clamp(sqrt(sqr_dist) / lightRange, 0.0, 1.0);
|
||||
highp float s2 = s * s;
|
||||
// controls curve by lerping between two variants (inverse-shape and inversequadratic-shape)
|
||||
highp float curveFactor = mix(s, s2, clamp(lightCurveFactor, 0.0, 1.0));
|
||||
highp float val = clamp(((1.0 - s2) * (1.0 - s2)) / (1.0 + lightFalloff * curveFactor), 0.0, 1.0);
|
||||
|
||||
val *= lightPower;
|
||||
val *= mask;
|
||||
|
||||
51
Robust.Client.WebView/Cef/WebViewManagerCef.Lock.cs
Normal file
51
Robust.Client.WebView/Cef/WebViewManagerCef.Lock.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Robust.Client.Utility;
|
||||
|
||||
namespace Robust.Client.WebView.Cef;
|
||||
|
||||
internal sealed partial class WebViewManagerCef
|
||||
{
|
||||
private const string BaseCacheName = "cef_cache";
|
||||
private const string LockFileName = "robust.lock";
|
||||
private FileStream? _lockFileStream;
|
||||
private const int MaxAttempts = 15; // This probably shouldn't be a cvar because the only reason you'd need it change for legit just botting the game.
|
||||
|
||||
private string FindAndLockCacheDirectory()
|
||||
{
|
||||
var rootDir = Path.Combine(UserDataDir.GetRootUserDataDir(_gameController), BaseCacheName);
|
||||
|
||||
for (var i = 0; i < MaxAttempts; i++)
|
||||
{
|
||||
var cacheDirPath = Path.Combine(rootDir, i.ToString());
|
||||
|
||||
if (TryLockCacheDir(i, cacheDirPath))
|
||||
return cacheDirPath;
|
||||
}
|
||||
|
||||
throw new Exception("Unable to locate available CEF cache directory!");
|
||||
}
|
||||
|
||||
private bool TryLockCacheDir(int attempt, string path)
|
||||
{
|
||||
_sawmill.Verbose($"Trying to lock cache directory {attempt}");
|
||||
|
||||
// Does not fail if directory already exists.
|
||||
Directory.CreateDirectory(path);
|
||||
|
||||
var lockFilePath = Path.Combine(path, LockFileName);
|
||||
|
||||
try
|
||||
{
|
||||
var file = File.Open(lockFilePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
|
||||
_lockFileStream = file;
|
||||
_sawmill.Debug($"Successfully locked CEF cache directory {attempt}");
|
||||
return true;
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_sawmill.Error($"Failed to lock cache directory {attempt}: {ex}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,12 +61,9 @@ namespace Robust.Client.WebView.Cef
|
||||
if (cefResourcesPath == null)
|
||||
throw new InvalidOperationException("Unable to locate cef_resources directory!");
|
||||
|
||||
var cachePath = "";
|
||||
if (_resourceManager.UserData is WritableDirProvider userData)
|
||||
{
|
||||
var rootDir = UserDataDir.GetRootUserDataDir(_gameController);
|
||||
cachePath = Path.Combine(rootDir, "cef_cache", "0");
|
||||
}
|
||||
var remoteDebugPort = _cfg.GetCVar(WCVars.WebRemoteDebugPort);
|
||||
|
||||
var cachePath = FindAndLockCacheDirectory();
|
||||
|
||||
var settings = new CefSettings()
|
||||
{
|
||||
@@ -76,7 +73,7 @@ namespace Robust.Client.WebView.Cef
|
||||
BrowserSubprocessPath = subProcessPath,
|
||||
LocalesDirPath = Path.Combine(cefResourcesPath, "locales"),
|
||||
ResourcesDirPath = cefResourcesPath,
|
||||
RemoteDebuggingPort = 9222,
|
||||
RemoteDebuggingPort = remoteDebugPort,
|
||||
CookieableSchemesList = "usr,res",
|
||||
CachePath = cachePath,
|
||||
};
|
||||
|
||||
@@ -26,4 +26,16 @@ public static class WCVars
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> WebHeadless =
|
||||
CVarDef.Create("web.headless", false, CVar.CLIENTONLY);
|
||||
|
||||
#if TOOLS
|
||||
private const int DefaultRemoteDebugPort = 9222;
|
||||
#else
|
||||
private const int DefaultRemoteDebugPort = 0;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// If not 0, the port number used for Chromium's remote debugging.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> WebRemoteDebugPort =
|
||||
CVarDef.Create("web.remote_debug_port", DefaultRemoteDebugPort, CVar.CLIENTONLY);
|
||||
}
|
||||
|
||||
@@ -54,8 +54,14 @@ namespace Robust.Client.Animations
|
||||
}
|
||||
else
|
||||
{
|
||||
var next = KeyFrames[nextKeyFrame];
|
||||
|
||||
// Get us a scale 0 -> 1 here.
|
||||
var t = playingTime / KeyFrames[nextKeyFrame].KeyTime;
|
||||
var t = playingTime / next.KeyTime;
|
||||
|
||||
// Apply easing to time parameter, if one was specified
|
||||
if (next.Easing != null)
|
||||
t = next.Easing(t);
|
||||
|
||||
switch (InterpolationMode)
|
||||
{
|
||||
@@ -147,10 +153,20 @@ namespace Robust.Client.Animations
|
||||
/// </summary>
|
||||
public readonly float KeyTime;
|
||||
|
||||
public KeyFrame(object value, float keyTime)
|
||||
/// <summary>
|
||||
/// An easing function to apply when interpolating to this keyframe's value.
|
||||
/// Modifies the time parameter (0..1) of the interpolation between the previous keyframe and this one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See <see cref="Easings"/> for examples of easing functions, or provide your own.
|
||||
/// </remarks>
|
||||
public readonly Func<float, float>? Easing;
|
||||
|
||||
public KeyFrame(object value, float keyTime, Func<float, float>? easing = null)
|
||||
{
|
||||
Value = value;
|
||||
KeyTime = keyTime;
|
||||
Easing = easing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,13 +372,13 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
return;
|
||||
}
|
||||
|
||||
var parentUid = xform.ParentUid;
|
||||
Vector2 worldPos;
|
||||
component.Volume = component.Params.Volume;
|
||||
|
||||
// Handle grid audio differently by using grid position.
|
||||
if ((component.Flags & AudioFlags.GridAudio) != 0x0)
|
||||
{
|
||||
var parentUid = xform.ParentUid;
|
||||
worldPos = _maps.GetGridPosition(parentUid);
|
||||
}
|
||||
else
|
||||
@@ -412,7 +412,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
}
|
||||
else
|
||||
{
|
||||
var occlusion = GetOcclusion(listener, delta, distance, entity);
|
||||
var occlusion = GetOcclusion(listener, delta, distance, parentUid);
|
||||
component.Occlusion = occlusion;
|
||||
}
|
||||
|
||||
@@ -420,11 +420,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
component.Position = worldPos;
|
||||
|
||||
// Make race cars go NYYEEOOOOOMMMMM
|
||||
if (_physicsQuery.TryGetComponent(entity, out var physicsComp))
|
||||
if (_physicsQuery.TryGetComponent(parentUid, out var physicsComp))
|
||||
{
|
||||
// This actually gets the tracked entity's xform & iterates up though the parents for the second time. Bit
|
||||
// inefficient.
|
||||
var velocity = _physics.GetMapLinearVelocity(entity, physicsComp, xform);
|
||||
var velocity = _physics.GetMapLinearVelocity(parentUid, physicsComp);
|
||||
component.Velocity = velocity;
|
||||
}
|
||||
}
|
||||
@@ -589,6 +589,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
|
||||
_xformSys.SetCoordinates(playing.Entity, new EntityCoordinates(entity, Vector2.Zero));
|
||||
|
||||
// Since we're playing the sound immediately in the middle of a tick, we need to force ProcessStream -now-
|
||||
// to set occlusion/position/velocity etc
|
||||
// otherwise predicted positional sounds will sound very incorrect in several possible ways (e#5802, e#6175) until the next tick
|
||||
ProcessStream(playing.Entity, playing.Component, Transform(playing.Entity), GetListenerCoordinates());
|
||||
|
||||
return playing;
|
||||
}
|
||||
|
||||
@@ -632,6 +637,10 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
|
||||
var playing = CreateAndStartPlayingStream(audioParams, specifier, stream);
|
||||
_xformSys.SetCoordinates(playing.Entity, coordinates);
|
||||
|
||||
// see PlayEntity for why this is necessary
|
||||
ProcessStream(playing.Entity, playing.Component, Transform(playing.Entity), GetListenerCoordinates());
|
||||
|
||||
return playing;
|
||||
}
|
||||
|
||||
@@ -714,8 +723,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
|
||||
offset = Math.Clamp(offset, 0f, maxOffset);
|
||||
source.PlaybackPosition = offset;
|
||||
|
||||
// For server we will rely on the adjusted one but locally we will have to adjust it ourselves.
|
||||
ApplyAudioParams(comp.Params, comp);
|
||||
source.StartPlaying();
|
||||
return (entity, comp);
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ internal sealed class ClientNetConfigurationManager : NetConfigurationManager, I
|
||||
// Actually set the CVar
|
||||
base.SetCVar(name, value, force);
|
||||
|
||||
if ((flags & CVar.REPLICATED) == 0)
|
||||
if ((flags & CVar.REPLICATED) == 0 || !NetManager.IsConnected)
|
||||
return;
|
||||
|
||||
var msg = new MsgConVars();
|
||||
|
||||
@@ -4,7 +4,7 @@ using Robust.Shared.ContentPack;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
#if DEBUG
|
||||
#if TOOLS
|
||||
internal sealed class DumpMetadataMembersCommand : LocalizedCommands
|
||||
{
|
||||
public override string Command => "dmetamem";
|
||||
|
||||
124
Robust.Client/Console/Commands/UITestCommand.TabWrapContainer.cs
Normal file
124
Robust.Client/Console/Commands/UITestCommand.TabWrapContainer.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Maths;
|
||||
using ItemJustification = Robust.Client.UserInterface.Controls.WrapContainer.ItemJustification;
|
||||
|
||||
namespace Robust.Client.Console.Commands;
|
||||
|
||||
internal sealed partial class UITestControl
|
||||
{
|
||||
private sealed class TabWrapContainer : Control
|
||||
{
|
||||
private readonly CheckBox _equalSizeBox;
|
||||
private readonly CheckBox _reverseBox;
|
||||
private readonly OptionButton _axisButton;
|
||||
private readonly OptionButton _justifyButton;
|
||||
private readonly LineEdit _separationEdit;
|
||||
private readonly LineEdit _crossSeparationEdit;
|
||||
|
||||
public TabWrapContainer()
|
||||
{
|
||||
var container = new WrapContainer
|
||||
{
|
||||
MouseFilter = MouseFilterMode.Stop,
|
||||
VerticalExpand = true,
|
||||
};
|
||||
|
||||
var random = new Random(3005);
|
||||
|
||||
for (var i = 0; i < 35; i++)
|
||||
{
|
||||
var val = random.Next(1, 16);
|
||||
|
||||
var text = string.Create(val, 0, (span, _) => span.Fill('O'));
|
||||
container.AddChild(new Button { Text = text });
|
||||
}
|
||||
|
||||
AddChild(new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Children =
|
||||
{
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 4,
|
||||
Children =
|
||||
{
|
||||
(_equalSizeBox = new CheckBox
|
||||
{
|
||||
Text = nameof(WrapContainer.EqualSize)
|
||||
}),
|
||||
(_reverseBox = new CheckBox
|
||||
{
|
||||
Text = nameof(WrapContainer.Reverse)
|
||||
}),
|
||||
(_axisButton = new OptionButton()),
|
||||
(_justifyButton = new OptionButton()),
|
||||
(_separationEdit = new LineEdit
|
||||
{
|
||||
PlaceHolder = "Separation",
|
||||
SetWidth = 100,
|
||||
}),
|
||||
(_crossSeparationEdit = new LineEdit
|
||||
{
|
||||
PlaceHolder = "Cross Separation",
|
||||
SetWidth = 100,
|
||||
})
|
||||
}
|
||||
},
|
||||
new PanelContainer
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat { BackgroundColor = Color.Black },
|
||||
Children =
|
||||
{
|
||||
container
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
_axisButton.AddItem(nameof(Axis.Horizontal), (int)Axis.Horizontal);
|
||||
_axisButton.AddItem(nameof(Axis.HorizontalReverse), (int)Axis.HorizontalReverse);
|
||||
_axisButton.AddItem(nameof(Axis.Vertical), (int)Axis.Vertical);
|
||||
_axisButton.AddItem(nameof(Axis.VerticalReverse), (int)Axis.VerticalReverse);
|
||||
|
||||
_axisButton.OnItemSelected += args =>
|
||||
{
|
||||
_axisButton.SelectId(args.Id);
|
||||
container.LayoutAxis = (Axis)args.Id;
|
||||
};
|
||||
|
||||
_justifyButton.AddItem(nameof(ItemJustification.Begin), (int)ItemJustification.Begin);
|
||||
_justifyButton.AddItem(nameof(ItemJustification.Center), (int)ItemJustification.Center);
|
||||
_justifyButton.AddItem(nameof(ItemJustification.End), (int)ItemJustification.End);
|
||||
|
||||
_justifyButton.OnItemSelected += args =>
|
||||
{
|
||||
_justifyButton.SelectId(args.Id);
|
||||
container.Justification = (ItemJustification)args.Id;
|
||||
};
|
||||
|
||||
_equalSizeBox.OnPressed += _ => container.EqualSize = _equalSizeBox.Pressed;
|
||||
_reverseBox.OnPressed += _ => container.Reverse = _reverseBox.Pressed;
|
||||
|
||||
_separationEdit.OnTextChanged += args =>
|
||||
{
|
||||
if (!int.TryParse(args.Text, out var sep))
|
||||
sep = 0;
|
||||
|
||||
container.SeparationOverride = sep;
|
||||
};
|
||||
|
||||
_crossSeparationEdit.OnTextChanged += args =>
|
||||
{
|
||||
if (!int.TryParse(args.Text, out var sep))
|
||||
sep = 0;
|
||||
|
||||
container.CrossSeparationOverride = sep;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@ using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -44,7 +42,10 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
|
||||
var progressBar = new ProgressBar { MaxValue = 10, Value = 5 };
|
||||
vBox.AddChild(progressBar);
|
||||
|
||||
var optionButton = new OptionButton();
|
||||
var optionButton = new OptionButton
|
||||
{
|
||||
ToolTip = "This button has a tooltip. Spooky!"
|
||||
};
|
||||
optionButton.AddItem("Honk");
|
||||
optionButton.AddItem("Foo");
|
||||
optionButton.AddItem("Bar");
|
||||
@@ -155,6 +156,7 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
|
||||
_sprite = new TabSpriteView();
|
||||
_tabContainer.AddChild(_sprite);
|
||||
_tabContainer.AddChild(TabCursorShapes());
|
||||
_tabContainer.AddChild(new TabWrapContainer { Name = nameof(Tab.WrapContainer) });
|
||||
}
|
||||
|
||||
public void OnClosed()
|
||||
@@ -275,32 +277,13 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
|
||||
RichText = 7,
|
||||
SpriteView = 8,
|
||||
TabCursorShapes = 9,
|
||||
WrapContainer = 10,
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UITestCommand : LocalizedCommands
|
||||
internal abstract class BaseUITestCommand : LocalizedCommands
|
||||
{
|
||||
public override string Command => "uitest";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var window = new DefaultWindow { MinSize = new(800, 600) };
|
||||
var control = new UITestControl();
|
||||
window.OnClose += control.OnClosed;
|
||||
window.Contents.AddChild(control);
|
||||
|
||||
window.OpenCentered();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UITest2Command : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _uiMgr = default!;
|
||||
|
||||
public override string Command => "uitest2";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
public sealed override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length > 1)
|
||||
{
|
||||
@@ -321,18 +304,10 @@ internal sealed class UITest2Command : LocalizedCommands
|
||||
control.SelectTab(tab);
|
||||
}
|
||||
|
||||
var window = _clyde.CreateWindow(new WindowCreateParameters
|
||||
{
|
||||
Title = Loc.GetString("cmd-uitest2-title"),
|
||||
});
|
||||
|
||||
var root = _uiMgr.CreateWindowRoot(window);
|
||||
window.DisposeOnClose = true;
|
||||
window.RequestClosed += _ => control.OnClosed();
|
||||
root.AddChild(control);
|
||||
CreateWindow(control);
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
public sealed override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
@@ -343,4 +318,35 @@ internal sealed class UITest2Command : LocalizedCommands
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
|
||||
protected abstract void CreateWindow(UITestControl control);
|
||||
}
|
||||
|
||||
internal sealed class UITestCommand : BaseUITestCommand
|
||||
{
|
||||
public override string Command => "uitest";
|
||||
|
||||
protected override void CreateWindow(UITestControl control)
|
||||
{
|
||||
var window = new DefaultWindow { MinSize = new(800, 600) };
|
||||
window.OnClose += control.OnClosed;
|
||||
window.Contents.AddChild(control);
|
||||
window.OpenCentered();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UITest2Command : BaseUITestCommand
|
||||
{
|
||||
public override string Command => "uitest2";
|
||||
|
||||
protected override void CreateWindow(UITestControl control)
|
||||
{
|
||||
var window = new OSWindow
|
||||
{
|
||||
Title = Loc.GetString("cmd-uitest2-title"),
|
||||
};
|
||||
window.AddChild(control);
|
||||
window.Closed += control.OnClosed;
|
||||
window.Show();
|
||||
}
|
||||
}
|
||||
|
||||
44
Robust.Client/Console/Commands/ViewportDebugCommands.cs
Normal file
44
Robust.Client/Console/Commands/ViewportDebugCommands.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
#if TOOLS
|
||||
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.Console.Commands;
|
||||
|
||||
internal sealed class ViewportClearAllCachedCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IClydeInternal _clyde = default!;
|
||||
|
||||
public string Command => "vp_clear_all_cached";
|
||||
public string Description => "Fires IClydeViewport.ClearCachedResources on all viewports";
|
||||
public string Help => "";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
_clyde.ViewportsClearAllCached();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ViewportTestFinalizeCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
|
||||
public string Command => "vp_test_finalize";
|
||||
public string Description => "Creates a viewport, renders it once, then leaks it (finalizes it).";
|
||||
public string Help => "";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var vp = _clyde.CreateViewport(new Vector2i(1920, 1080), nameof(ViewportTestFinalizeCommand));
|
||||
vp.Eye = _eyeManager.CurrentEye;
|
||||
|
||||
vp.Render();
|
||||
|
||||
// Leak it.
|
||||
}
|
||||
}
|
||||
|
||||
#endif // TOOLS
|
||||
@@ -8,6 +8,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using SDL3;
|
||||
|
||||
namespace Robust.Client
|
||||
{
|
||||
@@ -93,6 +94,8 @@ namespace Robust.Client
|
||||
|
||||
public void Run(DisplayMode mode, GameControllerOptions options, Func<ILogHandler>? logHandlerFactory = null)
|
||||
{
|
||||
_displayMode = mode;
|
||||
|
||||
if (!StartupSystemSplash(options, logHandlerFactory))
|
||||
{
|
||||
_logger.Fatal("Failed to start game controller!");
|
||||
|
||||
@@ -110,6 +110,8 @@ namespace Robust.Client
|
||||
|
||||
private ResourceManifestData? _resourceManifest;
|
||||
|
||||
private DisplayMode _displayMode;
|
||||
|
||||
public void SetCommandLineArgs(CommandLineArgs args)
|
||||
{
|
||||
_commandLineArgs = args;
|
||||
@@ -273,6 +275,9 @@ namespace Robust.Client
|
||||
}
|
||||
};
|
||||
|
||||
_configurationManager.OnValueChanged(CVars.DisplayMaxFPS, _ => UpdateVsyncConfig());
|
||||
_configurationManager.OnValueChanged(CVars.DisplayVSync, _ => UpdateVsyncConfig(), invokeImmediately: true);
|
||||
|
||||
_clyde.Ready();
|
||||
|
||||
if (_resourceManifest!.AutoConnect &&
|
||||
@@ -709,6 +714,30 @@ namespace Robust.Client
|
||||
}
|
||||
|
||||
|
||||
private void UpdateVsyncConfig()
|
||||
{
|
||||
if (_displayMode == DisplayMode.Headless)
|
||||
return;
|
||||
|
||||
var vsync = _configurationManager.GetCVar(CVars.DisplayVSync);
|
||||
var maxFps = Math.Clamp(_configurationManager.GetCVar(CVars.DisplayMaxFPS), 0, 10_000);
|
||||
|
||||
_clyde.VsyncEnabled = vsync;
|
||||
|
||||
if (_mainLoop == null)
|
||||
return;
|
||||
|
||||
if (vsync || maxFps == 0)
|
||||
{
|
||||
_mainLoop.SleepMode = SleepMode.None;
|
||||
}
|
||||
else
|
||||
{
|
||||
_mainLoop.SleepMode = SleepMode.Limit;
|
||||
_mainLoop.LimitMinFrameTime = TimeSpan.FromSeconds(1.0 / maxFps);
|
||||
}
|
||||
}
|
||||
|
||||
internal enum DisplayMode : byte
|
||||
{
|
||||
Headless,
|
||||
|
||||
@@ -29,6 +29,9 @@ namespace Robust.Client.GameObjects
|
||||
internal event Action? AfterStartup;
|
||||
internal event Action? AfterShutdown;
|
||||
|
||||
private readonly Queue<EntityUid> _queuedPredictedDeletions = new();
|
||||
private readonly HashSet<EntityUid> _queuedPredictedDeletionsSet = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SetupNetworking();
|
||||
@@ -213,6 +216,34 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
using (histogram?.WithLabels("PredictedQueueDel").NewTimer())
|
||||
{
|
||||
while (_queuedPredictedDeletions.TryDequeue(out var uid))
|
||||
{
|
||||
if (!MetaQuery.TryGetComponentInternal(uid, out var meta))
|
||||
continue;
|
||||
|
||||
if (meta.EntityLifeStage >= EntityLifeStage.Terminating)
|
||||
continue;
|
||||
|
||||
var xform = TransformQuery.GetComponentInternal(uid);
|
||||
if (meta.NetEntity.IsClientSide())
|
||||
{
|
||||
DeleteEntity(uid, meta, xform);
|
||||
}
|
||||
else
|
||||
{
|
||||
_xforms.DetachEntity(uid, xform, meta, null);
|
||||
// base call bypasses IGameTiming.InPrediction check
|
||||
// This is pretty janky and there should be a way for the client to dirty an entity outside of prediction
|
||||
// TODO PREDICTION
|
||||
base.Dirty(uid, xform, meta);
|
||||
}
|
||||
}
|
||||
|
||||
_queuedPredictedDeletionsSet.Clear();
|
||||
}
|
||||
|
||||
base.TickUpdate(frameTime, noPredictions, histogram);
|
||||
}
|
||||
|
||||
@@ -317,18 +348,23 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void PredictedQueueDeleteEntity(Entity<MetaDataComponent?, TransformComponent?> ent)
|
||||
{
|
||||
if (IsQueuedForDeletion(ent.Owner)
|
||||
|| !MetaQuery.Resolve(ent.Owner, ref ent.Comp1)
|
||||
|| ent.Comp1.EntityLifeStage >= EntityLifeStage.Terminating
|
||||
|| !TransformQuery.Resolve(ent.Owner, ref ent.Comp2))
|
||||
{
|
||||
return;
|
||||
}
|
||||
public override bool IsQueuedForDeletion(EntityUid uid)
|
||||
=> QueuedDeletionsSet.Contains(uid) || _queuedPredictedDeletions.Contains(uid);
|
||||
|
||||
if (ent.Comp1.NetEntity.IsClientSide())
|
||||
/// <inheritdoc />
|
||||
public override void PredictedQueueDeleteEntity(Entity<MetaDataComponent?> ent)
|
||||
{
|
||||
// Some UIs get disposed after entity-manager has shut down and already deleted all entities.
|
||||
if (!Started)
|
||||
return;
|
||||
|
||||
if (IsQueuedForDeletion(ent.Owner))
|
||||
return;
|
||||
|
||||
if (!MetaQuery.Resolve(ent.Owner, ref ent.Comp, false))
|
||||
return;
|
||||
|
||||
if (ent.Comp.NetEntity.IsClientSide())
|
||||
{
|
||||
// client-side QueueDeleteEntity re-fetches MetadataComp and checks IsClientSide().
|
||||
// base call to skip that.
|
||||
@@ -337,7 +373,10 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
else
|
||||
{
|
||||
_xforms.DetachEntity(ent.Owner, ent.Comp2);
|
||||
if (!_queuedPredictedDeletionsSet.Add(ent.Owner))
|
||||
return;
|
||||
|
||||
_queuedPredictedDeletions.Enqueue(ent.Owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,6 +294,16 @@ namespace Robust.Client.GameObjects
|
||||
LocalMatrix = Matrix3Helpers.CreateTransform(in offset, in rotation, in scale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If false, this will prevent any of this sprite's animated layers from looping their animation.
|
||||
/// This will set <see cref="Layer.AutoAnimated"/> whenever any layer's animation finishes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If this is false, this effectively overrides each layer's own <see cref="Layer.Loop"/>.
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
public bool Loop = true;
|
||||
|
||||
/// <summary>
|
||||
/// Update this sprite component to visibly match the current state of other at the time
|
||||
/// this is called. Does not keep them perpetually in sync.
|
||||
@@ -601,6 +611,7 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
layer.RenderingStrategy = layerDatum.RenderingStrategy ?? layer.RenderingStrategy;
|
||||
layer.Cycle = layerDatum.Cycle;
|
||||
layer.Loop = layerDatum.Loop;
|
||||
|
||||
layer.Color = layerDatum.Color ?? layer.Color;
|
||||
layer._rotation = layerDatum.Rotation ?? layer._rotation;
|
||||
@@ -1157,6 +1168,15 @@ namespace Robust.Client.GameObjects
|
||||
/// </remarks>
|
||||
[ViewVariables] public bool Cycle;
|
||||
|
||||
/// <summary>
|
||||
/// If false, this will prevent the layer's animation from looping.
|
||||
/// This will set <see cref="AutoAnimated"/> to false once the animation finishes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This may be overriden by the parent's loop property.
|
||||
/// </remarks>
|
||||
[ViewVariables] public bool Loop = true;
|
||||
|
||||
// TODO SPRITE ACCESS
|
||||
internal RSI.State? _actualState;
|
||||
[ViewVariables] public RSI.State? ActualState => _actualState;
|
||||
@@ -1336,6 +1356,8 @@ namespace Robust.Client.GameObjects
|
||||
DirOffset = toClone.DirOffset;
|
||||
_autoAnimated = toClone._autoAnimated;
|
||||
RenderingStrategy = toClone.RenderingStrategy;
|
||||
Cycle = toClone.Cycle;
|
||||
Loop = toClone.Loop;
|
||||
if (toClone.CopyToShaderParameters is { } copyToShaderParameters)
|
||||
CopyToShaderParameters = new CopyToShaderParameters(copyToShaderParameters);
|
||||
}
|
||||
@@ -1663,17 +1685,25 @@ namespace Robust.Client.GameObjects
|
||||
|
||||
internal void AdvanceFrameAnimation(RSI.State state)
|
||||
{
|
||||
// Can't advance frames without more than 1 delay which is already checked above.
|
||||
var delayCount = state.DelayCount;
|
||||
|
||||
while (AnimationTimeLeft < 0)
|
||||
{
|
||||
if (Reversed)
|
||||
{
|
||||
AnimationFrame -= 1;
|
||||
|
||||
// Animation finished, do we cycle back to positive or reset.
|
||||
if (AnimationFrame < 0)
|
||||
{
|
||||
if (!Loop || !_parent.Loop)
|
||||
{
|
||||
// stop at first frame
|
||||
AnimationFrame = 0;
|
||||
AnimationTimeLeft = 0;
|
||||
AutoAnimated = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Cycle)
|
||||
{
|
||||
AnimationFrame = 1;
|
||||
@@ -1691,9 +1721,17 @@ namespace Robust.Client.GameObjects
|
||||
{
|
||||
AnimationFrame += 1;
|
||||
|
||||
// Animation finished, do we reverse or reset.
|
||||
if (AnimationFrame >= delayCount)
|
||||
{
|
||||
if (!Loop || !_parent.Loop)
|
||||
{
|
||||
// stop at last frame
|
||||
AnimationFrame = delayCount - 1;
|
||||
AnimationTimeLeft = 0;
|
||||
AutoAnimated = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Cycle)
|
||||
{
|
||||
AnimationFrame = delayCount - 2;
|
||||
@@ -1711,6 +1749,7 @@ namespace Robust.Client.GameObjects
|
||||
AnimationTimeLeft += state.GetDelay(AnimationFrame);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics.Contracts;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Map;
|
||||
using Robust.Client.ResourceManagement;
|
||||
@@ -9,13 +10,14 @@ namespace Robust.Client.GameObjects;
|
||||
|
||||
public sealed class MapSystem : SharedMapSystem
|
||||
{
|
||||
protected override MapId GetNextMapId()
|
||||
[Pure]
|
||||
internal override MapId GetNextMapId()
|
||||
{
|
||||
// Client-side map entities use negative map Ids to avoid conflict with server-side maps.
|
||||
var id = new MapId(--LastMapId);
|
||||
var id = new MapId(LastMapId - 1);
|
||||
while (MapExists(id) || UsedIds.Contains(id))
|
||||
{
|
||||
id = new MapId(--LastMapId);
|
||||
id = new MapId(id.Value - 1);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,8 @@ namespace Robust.Client.GameObjects
|
||||
component.Enabled = state.Enabled;
|
||||
component.Offset = state.Offset;
|
||||
component.Softness = state.Softness;
|
||||
component.Falloff = state.Falloff;
|
||||
component.CurveFactor = state.CurveFactor;
|
||||
component.CastShadows = state.CastShadows;
|
||||
component.Energy = state.Energy;
|
||||
component.Radius = state.Radius;
|
||||
|
||||
@@ -88,6 +88,7 @@ public sealed partial class SpriteSystem
|
||||
|
||||
target.Comp.RenderOrder = source.Comp.RenderOrder;
|
||||
target.Comp.GranularLayersRendering = source.Comp.GranularLayersRendering;
|
||||
target.Comp.Loop = source.Comp.Loop;
|
||||
|
||||
DirtyBounds(target!);
|
||||
_tree.QueueTreeUpdate(target!);
|
||||
|
||||
@@ -220,24 +220,34 @@ Had full state: {LastFullState != null}"
|
||||
{
|
||||
var compState = change.State;
|
||||
|
||||
if (compState is IComponentDeltaState delta
|
||||
&& compData.TryGetValue(change.NetID, out var old)) // May fail if relying on implicit data
|
||||
if (compState is not IComponentDeltaState delta)
|
||||
{
|
||||
DebugTools.Assert(old is not IComponentDeltaState, "last state is not a full state");
|
||||
|
||||
if (cloneDelta)
|
||||
{
|
||||
compState = delta.CreateNewFullState(old!);
|
||||
}
|
||||
else
|
||||
{
|
||||
delta.ApplyToFullState(old!);
|
||||
compState = old;
|
||||
}
|
||||
DebugTools.Assert(compState is not IComponentDeltaState, "newly constructed state is not a full state");
|
||||
compData[change.NetID] = compState;
|
||||
continue;
|
||||
}
|
||||
|
||||
compData[change.NetID] = compState;
|
||||
if (!compData.TryGetValue(change.NetID, out var old))
|
||||
{
|
||||
// Either the server needs to ensure that the initial state it sends to a client is a full
|
||||
// state, or the client needs to be able to construct an implicit full state (i.e., get-state
|
||||
// code needs to be in shared code).
|
||||
//
|
||||
// Without this, the client won't be able to reset predicted changes made to this component.
|
||||
DebugTools.Assert("Received delta state without having received or constructed an implicit full state");
|
||||
continue;
|
||||
}
|
||||
|
||||
DebugTools.Assert(old is not IComponentDeltaState, "last state is not a full state");
|
||||
|
||||
if (!cloneDelta)
|
||||
{
|
||||
delta.ApplyToFullState(old!);
|
||||
continue;
|
||||
}
|
||||
|
||||
var newFull = delta.CreateNewFullState(old!);
|
||||
compData[change.NetID] = newFull;
|
||||
DebugTools.Assert(newFull is not IComponentDeltaState, "constructed state is not a full state");
|
||||
}
|
||||
|
||||
if (entityState.NetComponents == null)
|
||||
|
||||
@@ -121,6 +121,19 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
public void RenderNow(IRenderTarget renderTarget, Action<IRenderHandle> callback)
|
||||
{
|
||||
ClearRenderState();
|
||||
|
||||
_renderHandle.RenderInRenderTarget(
|
||||
renderTarget,
|
||||
() =>
|
||||
{
|
||||
callback(_renderHandle);
|
||||
},
|
||||
null);
|
||||
}
|
||||
|
||||
private void RenderSingleWorldOverlay(Overlay overlay, Viewport vp, OverlaySpace space, in Box2 worldBox, in Box2Rotated worldBounds)
|
||||
{
|
||||
// Check that entity manager has started.
|
||||
|
||||
@@ -451,6 +451,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var lastPower = float.NaN;
|
||||
var lastColor = new Color(float.NaN, float.NaN, float.NaN, float.NaN);
|
||||
var lastSoftness = float.NaN;
|
||||
var lastFalloff = float.NaN;
|
||||
var lastCurveFactor = float.NaN;
|
||||
Texture? lastMask = null;
|
||||
|
||||
using (_prof.Group("Draw Lights"))
|
||||
@@ -504,6 +506,18 @@ namespace Robust.Client.Graphics.Clyde
|
||||
lightShader.SetUniformMaybe("lightSoftness", lastSoftness);
|
||||
}
|
||||
|
||||
if (!MathHelper.CloseToPercent(lastFalloff, component.Falloff))
|
||||
{
|
||||
lastFalloff = component.Falloff;
|
||||
lightShader.SetUniformMaybe("lightFalloff", lastFalloff);
|
||||
}
|
||||
|
||||
if (!MathHelper.CloseToPercent(lastCurveFactor, component.CurveFactor))
|
||||
{
|
||||
lastCurveFactor = component.CurveFactor;
|
||||
lightShader.SetUniformMaybe("lightCurveFactor", lastCurveFactor);
|
||||
}
|
||||
|
||||
lightShader.SetUniformMaybe("lightCenter", lightPos);
|
||||
lightShader.SetUniformMaybe("lightIndex",
|
||||
component.CastShadows ? (i + 0.5f) / ShadowTexture.Height : -1);
|
||||
|
||||
@@ -209,6 +209,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
var pressure = estPixSize * size.X * size.Y;
|
||||
|
||||
var handle = AllocRid();
|
||||
var renderTarget = new RenderTexture(size, textureObject, this, handle);
|
||||
var data = new LoadedRenderTarget
|
||||
{
|
||||
IsWindow = false,
|
||||
@@ -220,10 +221,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
MemoryPressure = pressure,
|
||||
ColorFormat = format.ColorFormat,
|
||||
SampleParameters = sampleParameters,
|
||||
Instance = new WeakReference<RenderTargetBase>(renderTarget),
|
||||
Name = name,
|
||||
};
|
||||
|
||||
//GC.AddMemoryPressure(pressure);
|
||||
var renderTarget = new RenderTexture(size, textureObject, this, handle);
|
||||
_renderTargets.Add(handle, data);
|
||||
return renderTarget;
|
||||
}
|
||||
@@ -301,10 +303,22 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LoadedRenderTarget
|
||||
public IEnumerable<(RenderTargetBase, LoadedRenderTarget)> GetLoadedRenderTextures()
|
||||
{
|
||||
foreach (var loaded in _renderTargets.Values)
|
||||
{
|
||||
if (!loaded.Instance.TryGetTarget(out var instance))
|
||||
continue;
|
||||
|
||||
yield return (instance, loaded);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class LoadedRenderTarget
|
||||
{
|
||||
public bool IsWindow;
|
||||
public WindowId WindowId;
|
||||
public string? Name;
|
||||
|
||||
public Vector2i Size;
|
||||
public bool IsSrgb;
|
||||
@@ -325,9 +339,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public long MemoryPressure;
|
||||
|
||||
public TextureSampleParameters? SampleParameters;
|
||||
|
||||
public required WeakReference<RenderTargetBase> Instance;
|
||||
}
|
||||
|
||||
private abstract class RenderTargetBase : IRenderTarget
|
||||
internal abstract class RenderTargetBase : IRenderTarget
|
||||
{
|
||||
protected readonly Clyde Clyde;
|
||||
private bool _disposed;
|
||||
@@ -389,7 +405,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RenderTexture : RenderTargetBase, IRenderTexture
|
||||
internal sealed class RenderTexture : RenderTargetBase, IRenderTexture
|
||||
{
|
||||
public RenderTexture(Vector2i size, ClydeTexture texture, Clyde clyde, ClydeHandle handle)
|
||||
: base(clyde, handle)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
@@ -15,10 +16,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
private readonly Dictionary<ClydeHandle, WeakReference<Viewport>> _viewports =
|
||||
new();
|
||||
|
||||
private long _nextViewportId = 1;
|
||||
|
||||
private readonly ConcurrentQueue<ViewportDisposeData> _viewportDisposeQueue = new();
|
||||
|
||||
private Viewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters = default, string? name = null)
|
||||
{
|
||||
var handle = AllocRid();
|
||||
var viewport = new Viewport(handle, name, this)
|
||||
var viewport = new Viewport(_nextViewportId++, handle, name, this)
|
||||
{
|
||||
Size = size,
|
||||
RenderTarget = CreateRenderTarget(size,
|
||||
@@ -59,28 +64,43 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private void FlushViewportDispose()
|
||||
{
|
||||
// Free of allocations unless a dead viewport is found.
|
||||
List<ClydeHandle>? toRemove = null;
|
||||
foreach (var (handle, viewportRef) in _viewports)
|
||||
while (_viewportDisposeQueue.TryDequeue(out var data))
|
||||
{
|
||||
if (!viewportRef.TryGetTarget(out _))
|
||||
{
|
||||
toRemove ??= new List<ClydeHandle>();
|
||||
toRemove.Add(handle);
|
||||
}
|
||||
}
|
||||
|
||||
if (toRemove == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var remove in toRemove)
|
||||
{
|
||||
_viewports.Remove(remove);
|
||||
DisposeViewport(data);
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeViewport(ViewportDisposeData disposeData)
|
||||
{
|
||||
_clydeSawmill.Warning($"Viewport {disposeData.Id} got leaked");
|
||||
|
||||
_viewports.Remove(disposeData.Handle);
|
||||
if (disposeData.ClearEvent is not { } clearEvent)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
clearEvent(disposeData.ClearEventData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_clydeSawmill.Error($"Caught exception while disposing viewport: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
#if TOOLS
|
||||
public void ViewportsClearAllCached()
|
||||
{
|
||||
foreach (var vpRef in _viewports.Values)
|
||||
{
|
||||
if (!vpRef.TryGetTarget(out var vp))
|
||||
continue;
|
||||
|
||||
vp.FireClear();
|
||||
}
|
||||
}
|
||||
#endif // TOOLS
|
||||
|
||||
private sealed class Viewport : IClydeViewport
|
||||
{
|
||||
private readonly ClydeHandle _handle;
|
||||
@@ -106,17 +126,20 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
public string? Name { get; }
|
||||
|
||||
public Viewport(ClydeHandle handle, string? name, Clyde clyde)
|
||||
public Viewport(long id, ClydeHandle handle, string? name, Clyde clyde)
|
||||
{
|
||||
Name = name;
|
||||
_handle = handle;
|
||||
_clyde = clyde;
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public Vector2i Size { get; set; }
|
||||
public event Action<ClearCachedViewportResourcesEvent>? ClearCachedResources;
|
||||
public Color? ClearColor { get; set; } = Color.Black;
|
||||
public Vector2 RenderScale { get; set; } = Vector2.One;
|
||||
public bool AutomaticRender { get; set; }
|
||||
public long Id { get; }
|
||||
|
||||
void IClydeViewport.Render()
|
||||
{
|
||||
@@ -186,20 +209,56 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_clyde.RenderOverlaysDirect(this, control, handle, OverlaySpace.ScreenSpace, viewportBounds);
|
||||
}
|
||||
|
||||
~Viewport()
|
||||
{
|
||||
_clyde._viewportDisposeQueue.Enqueue(DisposeData(referenceSelf: false));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
RenderTarget.Dispose();
|
||||
LightRenderTarget.Dispose();
|
||||
WallMaskRenderTarget.Dispose();
|
||||
WallBleedIntermediateRenderTarget1.Dispose();
|
||||
WallBleedIntermediateRenderTarget2.Dispose();
|
||||
|
||||
_clyde._viewports.Remove(_handle);
|
||||
_clyde.DisposeViewport(DisposeData(referenceSelf: false));
|
||||
}
|
||||
|
||||
private ViewportDisposeData DisposeData(bool referenceSelf)
|
||||
{
|
||||
return new ViewportDisposeData
|
||||
{
|
||||
Handle = _handle,
|
||||
Id = Id,
|
||||
ClearEvent = ClearCachedResources,
|
||||
ClearEventData = MakeClearEvent(referenceSelf)
|
||||
};
|
||||
}
|
||||
|
||||
private ClearCachedViewportResourcesEvent MakeClearEvent(bool referenceSelf)
|
||||
{
|
||||
return new ClearCachedViewportResourcesEvent(Id, referenceSelf ? this : null);
|
||||
}
|
||||
|
||||
public void FireClear()
|
||||
{
|
||||
ClearCachedResources?.Invoke(MakeClearEvent(referenceSelf: true));
|
||||
}
|
||||
|
||||
IRenderTexture IClydeViewport.RenderTarget => RenderTarget;
|
||||
IRenderTexture IClydeViewport.LightRenderTarget => LightRenderTarget;
|
||||
public IEye? Eye { get; set; }
|
||||
}
|
||||
|
||||
private sealed class ViewportDisposeData
|
||||
{
|
||||
public ClydeHandle Handle;
|
||||
public long Id;
|
||||
public Action<ClearCachedViewportResourcesEvent>? ClearEvent;
|
||||
public ClearCachedViewportResourcesEvent ClearEventData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,15 +354,17 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_windowHandles.Add(reg.Handle);
|
||||
|
||||
var rtId = AllocRid();
|
||||
var renderTarget = new RenderWindow(this, rtId);
|
||||
_renderTargets.Add(rtId, new LoadedRenderTarget
|
||||
{
|
||||
Size = reg.FramebufferSize,
|
||||
IsWindow = true,
|
||||
WindowId = reg.Id,
|
||||
IsSrgb = true
|
||||
IsSrgb = true,
|
||||
Instance = new WeakReference<RenderTargetBase>(renderTarget),
|
||||
});
|
||||
|
||||
reg.RenderTarget = new RenderWindow(this, rtId);
|
||||
reg.RenderTarget = renderTarget;
|
||||
|
||||
_glContext!.WindowCreated(glSpec, reg);
|
||||
}
|
||||
@@ -405,10 +407,17 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_glContext?.SwapAllBuffers();
|
||||
}
|
||||
|
||||
private void VSyncChanged(bool newValue)
|
||||
public bool VsyncEnabled
|
||||
{
|
||||
_vSync = newValue;
|
||||
_glContext?.UpdateVSync();
|
||||
get => _vSync;
|
||||
set
|
||||
{
|
||||
if (_vSync == value)
|
||||
return;
|
||||
|
||||
_vSync = value;
|
||||
_glContext?.UpdateVSync();
|
||||
}
|
||||
}
|
||||
|
||||
private void WindowModeChanged(int mode)
|
||||
|
||||
@@ -114,7 +114,6 @@ namespace Robust.Client.Graphics.Clyde
|
||||
_proto.PrototypesReloaded += OnProtoReload;
|
||||
|
||||
_cfg.OnValueChanged(CVars.DisplayOGLCheckErrors, b => _checkGLErrors = b, true);
|
||||
_cfg.OnValueChanged(CVars.DisplayVSync, VSyncChanged, true);
|
||||
_cfg.OnValueChanged(CVars.DisplayWindowMode, WindowModeChanged, true);
|
||||
_cfg.OnValueChanged(CVars.LightResolutionScale, LightResolutionScaleChanged, true);
|
||||
_cfg.OnValueChanged(CVars.MaxShadowcastingLights, MaxShadowcastingLightsChanged, true);
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public bool IsFocused => true;
|
||||
private readonly List<IClydeWindow> _windows = new();
|
||||
private int _nextWindowId = 2;
|
||||
private long _nextViewportId = 1;
|
||||
|
||||
public ShaderInstance InstanceShader(ShaderSourceResource handle, bool? light = null, ShaderBlendMode? blend = null)
|
||||
{
|
||||
@@ -75,6 +76,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
return [];
|
||||
}
|
||||
|
||||
public IEnumerable<(Clyde.RenderTargetBase, Clyde.LoadedRenderTarget)> GetLoadedRenderTextures()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public ClydeDebugLayers DebugLayers { get; set; }
|
||||
|
||||
public string GetKeyName(Keyboard.Key key) => string.Empty;
|
||||
@@ -240,7 +246,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
public IClydeViewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters,
|
||||
string? name = null)
|
||||
{
|
||||
return new Viewport(size);
|
||||
return new Viewport(_nextViewportId++, size);
|
||||
}
|
||||
|
||||
public IEnumerable<IClydeMonitor> EnumerateMonitors()
|
||||
@@ -307,6 +313,19 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
public IFileDialogManagerImplementation? FileDialogImpl => null;
|
||||
|
||||
public bool VsyncEnabled { get; set; }
|
||||
|
||||
#if TOOLS
|
||||
public void ViewportsClearAllCached()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
#endif // TOOLS
|
||||
|
||||
public void RenderNow(IRenderTarget renderTarget, Action<IRenderHandle> callback)
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class DummyCursor : ICursor
|
||||
{
|
||||
public void Dispose()
|
||||
@@ -482,15 +501,19 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private sealed class Viewport : IClydeViewport
|
||||
{
|
||||
public Viewport(Vector2i size)
|
||||
public Viewport(long id, Vector2i size)
|
||||
{
|
||||
Size = size;
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ClearCachedResources?.Invoke(new ClearCachedViewportResourcesEvent(Id, null));
|
||||
}
|
||||
|
||||
public long Id { get; }
|
||||
|
||||
public IRenderTexture RenderTarget { get; } =
|
||||
new DummyRenderTexture(Vector2i.One, new DummyTexture(Vector2i.One));
|
||||
|
||||
@@ -499,6 +522,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
public IEye? Eye { get; set; }
|
||||
public Vector2i Size { get; }
|
||||
public event Action<ClearCachedViewportResourcesEvent>? ClearCachedResources;
|
||||
public Color? ClearColor { get; set; } = Color.Black;
|
||||
public Vector2 RenderScale { get; set; }
|
||||
public bool AutomaticRender { get; set; }
|
||||
|
||||
@@ -328,11 +328,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
reg.RenderTexture?.Dispose();
|
||||
|
||||
reg.RenderTexture = Clyde.CreateRenderTarget(reg.Reg.FramebufferSize, new RenderTargetFormatParameters
|
||||
{
|
||||
ColorFormat = RenderTargetColorFormat.Rgba8Srgb,
|
||||
HasDepthStencil = true
|
||||
});
|
||||
reg.RenderTexture = Clyde.CreateRenderTarget(
|
||||
reg.Reg.FramebufferSize,
|
||||
new RenderTargetFormatParameters
|
||||
{
|
||||
ColorFormat = RenderTargetColorFormat.Rgba8Srgb,
|
||||
HasDepthStencil = true
|
||||
},
|
||||
name: $"{reg.Reg.Id}-RenderTexture");
|
||||
// Necessary to correctly sync multi-context blitting.
|
||||
reg.RenderTexture.MakeGLFence = true;
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/* SDL3-CS - C# Bindings for SDL3
|
||||
*
|
||||
* Copyright (c) 2024 Colin Jackson
|
||||
*
|
||||
* This software is provided 'as-is', without any express or implied warranty.
|
||||
* In no event will the authors be held liable for any damages arising from
|
||||
* the use of this software.
|
||||
*
|
||||
* Permission is granted to anyone to use this software for any purpose,
|
||||
* including commercial applications, and to alter it and redistribute it
|
||||
* freely, subject to the following restrictions:
|
||||
*
|
||||
* 1. The origin of this software must not be misrepresented; you must not
|
||||
* claim that you wrote the original software. If you use this software in a
|
||||
* product, an acknowledgment in the product documentation would be
|
||||
* appreciated but is not required.
|
||||
*
|
||||
* 2. Altered source versions must be plainly marked as such, and must not be
|
||||
* misrepresented as being the original software.
|
||||
*
|
||||
* 3. This notice may not be removed or altered from any source distribution.
|
||||
*
|
||||
* Colin "cryy22" Jackson <c@cryy22.art>
|
||||
*
|
||||
*/
|
||||
@@ -1,57 +0,0 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SDL3;
|
||||
|
||||
public static partial class SDL
|
||||
{
|
||||
// Extensions to SDL3-CS that aren't part of the main library.
|
||||
|
||||
[LibraryImport(nativeLibName)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
public static unsafe partial void SDL_SetLogOutputFunction(delegate* unmanaged[Cdecl] <void*, int, SDL_LogPriority, byte*, void> callback, void* userdata);
|
||||
|
||||
[LibraryImport(nativeLibName)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
public static unsafe partial SDLBool SDL_AddEventWatch(delegate* unmanaged[Cdecl] <void*, SDL_Event*, byte> filter, void* userdata);
|
||||
|
||||
[LibraryImport(nativeLibName)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
public static unsafe partial void SDL_RemoveEventWatch(delegate* unmanaged[Cdecl] <void*, SDL_Event*, byte> filter, void* userdata);
|
||||
|
||||
[LibraryImport(nativeLibName, StringMarshalling = StringMarshalling.Utf8)]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
public static unsafe partial void SDL_ShowFileDialogWithProperties(int type, delegate* unmanaged[Cdecl]<void*, byte**, int, void> callback, void* userdata, uint properties);
|
||||
|
||||
[LibraryImport(nativeLibName, EntryPoint = "SDL_WaitEvent")]
|
||||
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
|
||||
public static partial SDLBool SDL_WaitEventRef(ref SDL_Event @event);
|
||||
|
||||
public const byte SDL_BUTTON_LEFT = 1;
|
||||
public const byte SDL_BUTTON_MIDDLE = 2;
|
||||
public const byte SDL_BUTTON_RIGHT = 3;
|
||||
public const byte SDL_BUTTON_X1 = 4;
|
||||
public const byte SDL_BUTTON_X2 = 5;
|
||||
|
||||
public const int SDL_GL_CONTEXT_PROFILE_CORE = 0x0001;
|
||||
public const int SDL_GL_CONTEXT_PROFILE_COMPATIBILITY = 0x0002;
|
||||
public const int SDL_GL_CONTEXT_PROFILE_ES = 0x0004;
|
||||
|
||||
public const int SDL_GL_CONTEXT_DEBUG_FLAG = 0x0001;
|
||||
public const int SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG = 0x0002;
|
||||
public const int SDL_GL_CONTEXT_ROBUST_ACCESS_FLAG = 0x0004;
|
||||
public const int SDL_GL_CONTEXT_RESET_ISOLATION_FLAG = 0x0008;
|
||||
|
||||
public const int SDL_FILEDIALOG_OPENFILE = 0;
|
||||
public const int SDL_FILEDIALOG_SAVEFILE = 1;
|
||||
public const int SDL_FILEDIALOG_OPENFOLDER = 2;
|
||||
|
||||
public const string SDL_PROP_FILE_DIALOG_NFILTERS_NUMBER = "SDL.filedialog.nfilters";
|
||||
public const string SDL_PROP_FILE_DIALOG_FILTERS_POINTER = "SDL.filedialog.filters";
|
||||
public const string SDL_PROP_FILE_DIALOG_WINDOW_POINTER = "SDL.filedialog.window";
|
||||
|
||||
public static int SDL_VERSIONNUM_MAJOR(int version) => version / 1000000;
|
||||
public static int SDL_VERSIONNUM_MINOR(int version) => version / 1000 % 1000;
|
||||
public static int SDL_VERSIONNUM_MICRO(int version) => version % 1000;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -55,6 +55,7 @@ namespace Robust.Client.Graphics
|
||||
|
||||
Texture GetStockTexture(ClydeStockTexture stockTexture);
|
||||
IEnumerable<(Clyde.Clyde.ClydeTexture, Clyde.Clyde.LoadedTexture)> GetLoadedTextures();
|
||||
IEnumerable<(Clyde.Clyde.RenderTargetBase, Clyde.Clyde.LoadedRenderTarget)> GetLoadedRenderTextures();
|
||||
|
||||
ClydeDebugLayers DebugLayers { get; set; }
|
||||
|
||||
@@ -72,5 +73,20 @@ namespace Robust.Client.Graphics
|
||||
void RunOnWindowThread(Action action);
|
||||
|
||||
IFileDialogManagerImplementation? FileDialogImpl { get; }
|
||||
|
||||
bool VsyncEnabled { get; set; }
|
||||
|
||||
// Viewports
|
||||
|
||||
#if TOOLS
|
||||
|
||||
/// <summary>
|
||||
/// Fires <see cref="IClydeViewport.ClearCachedResources"/> on all viewports. For debugging.
|
||||
/// </summary>
|
||||
void ViewportsClearAllCached();
|
||||
|
||||
#endif // TOOLS
|
||||
|
||||
void RenderNow(IRenderTarget renderTarget, Action<IRenderHandle> callback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,11 @@ namespace Robust.Client.Graphics
|
||||
/// </summary>
|
||||
public interface IClydeViewport : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// A unique ID for this viewport. No other viewport with this ID can ever exist in the app lifetime.
|
||||
/// </summary>
|
||||
long Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The render target that is rendered to when rendering this viewport.
|
||||
/// </summary>
|
||||
@@ -22,6 +27,16 @@ namespace Robust.Client.Graphics
|
||||
IEye? Eye { get; set; }
|
||||
Vector2i Size { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Raised when the viewport indicates that any cached rendering resources (e.g. render targets)
|
||||
/// should be purged.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This event is raised if the viewport is disposed (manually or via finalization).
|
||||
/// However, code should expect this event to be raised at any time, even if the viewport is not disposed fully.
|
||||
/// </remarks>
|
||||
event Action<ClearCachedViewportResourcesEvent> ClearCachedResources;
|
||||
|
||||
/// <summary>
|
||||
/// Color to clear the render target to before rendering. If null, no clearing will happen.
|
||||
/// </summary>
|
||||
@@ -85,4 +100,23 @@ namespace Robust.Client.Graphics
|
||||
IViewportControl control,
|
||||
in UIBox2i viewportBounds);
|
||||
}
|
||||
|
||||
public struct ClearCachedViewportResourcesEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IClydeViewport.Id"/> of the viewport.
|
||||
/// </summary>
|
||||
public readonly long ViewportId;
|
||||
|
||||
/// <summary>
|
||||
/// The viewport itself. This is not available if the viewport was disposed.
|
||||
/// </summary>
|
||||
public readonly IClydeViewport? Viewport;
|
||||
|
||||
internal ClearCachedViewportResourcesEvent(long viewportId, IClydeViewport? viewport)
|
||||
{
|
||||
ViewportId = viewportId;
|
||||
Viewport = viewport;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -562,7 +562,7 @@ namespace Robust.Client.Placement
|
||||
}
|
||||
|
||||
coordinates = InputManager.MouseScreenPosition;
|
||||
return true;
|
||||
return coordinates.IsValid;
|
||||
}
|
||||
|
||||
private bool CurrentEraserMouseCoordinates(out EntityCoordinates coordinates)
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace Robust.Client.Replays.Commands;
|
||||
public abstract class BaseReplayCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] protected readonly IReplayPlaybackManager PlaybackManager = default!;
|
||||
protected ILocalizationManager Loc => LocalizationManager;
|
||||
|
||||
public override string Description => Loc.GetString($"cmd-{Command.Replace('_','-')}-desc");
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<PackageReference Include="OpenToolkit.Graphics" PrivateAssets="compile" />
|
||||
<PackageReference Include="OpenTK.Audio.OpenAL" PrivateAssets="compile" />
|
||||
<PackageReference Include="SpaceWizards.SharpFont" PrivateAssets="compile" />
|
||||
<PackageReference Include="SpaceWizards.Sdl" PrivateAssets="compile" />
|
||||
<PackageReference Include="Robust.Natives" />
|
||||
<PackageReference Include="TerraFX.Interop.Windows" PrivateAssets="compile" />
|
||||
<PackageReference Condition="'$(RobustToolsBuild)' == 'True'" Include="JetBrains.Profiler.Api" PrivateAssets="compile" />
|
||||
@@ -63,6 +64,7 @@
|
||||
<RobustLinkAssemblies Include="TerraFX.Interop.Windows" />
|
||||
<RobustLinkAssemblies Include="TerraFX.Interop.Xlib" />
|
||||
<RobustLinkAssemblies Include="OpenToolkit.Graphics" />
|
||||
<RobustLinkAssemblies Include="SpaceWizards.Sdl" />
|
||||
<RobustLinkAssemblies Include="SpaceWizards.SharpFont" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
192
Robust.Client/UserInterface/Axis.cs
Normal file
192
Robust.Client/UserInterface/Axis.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
/// <summary>
|
||||
/// Defines an axis that certain controls can be laid out along.
|
||||
/// </summary>
|
||||
/// <seealso cref="IAxisImplementation"/>
|
||||
public enum Axis : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Items are laid out left to right.
|
||||
/// </summary>
|
||||
Horizontal,
|
||||
|
||||
/// <summary>
|
||||
/// Items are laid out right to left.
|
||||
/// </summary>
|
||||
HorizontalReverse,
|
||||
|
||||
/// <summary>
|
||||
/// Items are laid out top to bottom.
|
||||
/// </summary>
|
||||
Vertical,
|
||||
|
||||
/// <summary>
|
||||
/// Items are laid out bottom to top.
|
||||
/// </summary>
|
||||
VerticalReverse,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface that implements the rules of an <see cref="Axis"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// To make it easier to write code that supports all 4 layout axis, layout code is advised to use generics over this
|
||||
/// type and its implementors.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// An axis has a "main" and a "cross" axis. For example,
|
||||
/// <see cref="HorizontalAxis"/> has the main axis go left to right, and the cross axis go top to bottom.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The functions in this interface primarily allow converting between "UI space" (the normal UI coordinate system) and
|
||||
/// "axis space" (same as UI space for <see cref="HorizontalAxis"/>). This allows you to write all code as if you're
|
||||
/// doing only horizontal layout, but automatically have it work on all axis.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="HorizontalAxis"/>
|
||||
/// <seealso cref="HorizontalReverseAxis"/>
|
||||
/// <seealso cref="VerticalAxis"/>
|
||||
/// <seealso cref="VerticalReverseAxis"/>
|
||||
public interface IAxisImplementation
|
||||
{
|
||||
//
|
||||
// To/from axis space conversions
|
||||
//
|
||||
|
||||
/// <summary>
|
||||
/// Convert a size value (e.g. from <see cref="Control.DesiredSize"/>) from UI space to axis space.
|
||||
/// </summary>
|
||||
static abstract Vector2 SizeToAxis(Vector2 size);
|
||||
|
||||
/// <summary>
|
||||
/// Convert a size value (e.g. for <see cref="Control.Measure"/>) from axis space to UI space.
|
||||
/// </summary>
|
||||
static abstract Vector2 SizeFromAxis(Vector2 size);
|
||||
|
||||
/// <summary>
|
||||
/// Convert a box (e.g. for <see cref="Control.Arrange"/>) from axis space to UI space.
|
||||
/// </summary>
|
||||
/// <param name="box">The box to convert, in axis space.</param>
|
||||
/// <param name="spaceSize">The amount of space, in UI space, that the layout is happening relative to.</param>
|
||||
static abstract UIBox2 BoxFromAxis(UIBox2 box, Vector2 spaceSize);
|
||||
|
||||
//
|
||||
// Control
|
||||
//
|
||||
|
||||
/// <summary>
|
||||
/// Gets the "expand flag" (<see cref="Control.HorizontalExpand"/> or <see cref="Control.VerticalExpand"/>) for a
|
||||
/// control that is appropriate for the main axis.
|
||||
/// </summary>
|
||||
static abstract bool GetMainExpandFlag(Control control);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Axis implementation for <see cref="Axis.Horizontal"/>.
|
||||
/// </summary>
|
||||
public struct HorizontalAxis : IAxisImplementation
|
||||
{
|
||||
public static Vector2 SizeToAxis(Vector2 size)
|
||||
{
|
||||
return size;
|
||||
}
|
||||
|
||||
public static Vector2 SizeFromAxis(Vector2 size)
|
||||
{
|
||||
return size;
|
||||
}
|
||||
|
||||
public static UIBox2 BoxFromAxis(UIBox2 box, Vector2 spaceSize)
|
||||
{
|
||||
return box;
|
||||
}
|
||||
|
||||
public static bool GetMainExpandFlag(Control control)
|
||||
{
|
||||
return control.HorizontalExpand;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Axis implementation for <see cref="Axis.HorizontalReverse"/>.
|
||||
/// </summary>
|
||||
public struct HorizontalReverseAxis : IAxisImplementation
|
||||
{
|
||||
public static Vector2 SizeToAxis(Vector2 size)
|
||||
{
|
||||
return size;
|
||||
}
|
||||
|
||||
public static Vector2 SizeFromAxis(Vector2 size)
|
||||
{
|
||||
return size;
|
||||
}
|
||||
|
||||
public static UIBox2 BoxFromAxis(UIBox2 box, Vector2 spaceSize)
|
||||
{
|
||||
return new UIBox2(spaceSize.X - box.Right, box.Top, spaceSize.X - box.Left, box.Bottom);
|
||||
}
|
||||
|
||||
public static bool GetMainExpandFlag(Control control)
|
||||
{
|
||||
return control.HorizontalExpand;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Axis implementation for <see cref="Axis.Vertical"/>.
|
||||
/// </summary>
|
||||
public struct VerticalAxis : IAxisImplementation
|
||||
{
|
||||
public static Vector2 SizeToAxis(Vector2 size)
|
||||
{
|
||||
return new Vector2(size.Y, size.X);
|
||||
}
|
||||
|
||||
public static Vector2 SizeFromAxis(Vector2 size)
|
||||
{
|
||||
return new Vector2(size.Y, size.X);
|
||||
}
|
||||
|
||||
public static UIBox2 BoxFromAxis(UIBox2 box, Vector2 spaceSize)
|
||||
{
|
||||
return new UIBox2(box.Top, box.Left, box.Bottom, box.Right);
|
||||
}
|
||||
|
||||
public static bool GetMainExpandFlag(Control control)
|
||||
{
|
||||
return control.VerticalExpand;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Axis implementation for <see cref="Axis.VerticalReverse"/>.
|
||||
/// </summary>
|
||||
public struct VerticalReverseAxis : IAxisImplementation
|
||||
{
|
||||
public static Vector2 SizeToAxis(Vector2 size)
|
||||
{
|
||||
return new Vector2(size.Y, size.X);
|
||||
}
|
||||
|
||||
public static Vector2 SizeFromAxis(Vector2 size)
|
||||
{
|
||||
return new Vector2(size.Y, size.X);
|
||||
}
|
||||
|
||||
public static UIBox2 BoxFromAxis(UIBox2 box, Vector2 spaceSize)
|
||||
{
|
||||
return new UIBox2(box.Top, spaceSize.Y - box.Right, box.Bottom, spaceSize.Y - box.Left);
|
||||
}
|
||||
|
||||
public static bool GetMainExpandFlag(Control control)
|
||||
{
|
||||
return control.VerticalExpand;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1049,7 +1049,7 @@ namespace Robust.Client.UserInterface
|
||||
Ignore = 2,
|
||||
}
|
||||
|
||||
public sealed class OrderedChildCollection : ICollection<Control>, IReadOnlyCollection<Control>
|
||||
public sealed class OrderedChildCollection : ICollection<Control>, IReadOnlyList<Control>
|
||||
{
|
||||
private readonly Control Owner;
|
||||
|
||||
@@ -1101,6 +1101,7 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
int ICollection<Control>.Count => Owner.ChildCount;
|
||||
int IReadOnlyCollection<Control>.Count => Owner.ChildCount;
|
||||
public Control this[int index] => Owner._orderedChildren[index];
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
|
||||
@@ -27,8 +27,6 @@ namespace Robust.Client.UserInterface.Controls
|
||||
/// </remarks>
|
||||
public AlignMode Align { get; set; }
|
||||
|
||||
private bool Vertical => Orientation == LayoutOrientation.Vertical;
|
||||
|
||||
public LayoutOrientation Orientation
|
||||
{
|
||||
get => _orientation;
|
||||
@@ -56,19 +54,24 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
// Account for separation.
|
||||
var separation = ActualSeparation * (Children.Where(c => c.Visible).Count() - 1);
|
||||
var desiredSize = Vector2.Zero;
|
||||
if (Vertical)
|
||||
if (Orientation == LayoutOrientation.Vertical)
|
||||
{
|
||||
desiredSize.Y += separation;
|
||||
availableSize.Y = Math.Max(0, availableSize.Y - separation);
|
||||
return MeasureItems<VerticalAxis>(availableSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
desiredSize.X += separation;
|
||||
availableSize.X = Math.Max(0, availableSize.X - separation);
|
||||
return MeasureItems<HorizontalAxis>(availableSize);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 MeasureItems<TAxis>(Vector2 availableSize) where TAxis : IAxisImplementation
|
||||
{
|
||||
availableSize = TAxis.SizeToAxis(availableSize);
|
||||
|
||||
// Account for separation.
|
||||
var separation = ActualSeparation * (Children.Where(c => c.Visible).Count() - 1);
|
||||
var desiredSize = new Vector2(separation, 0);
|
||||
availableSize.X = Math.Max(0, availableSize.X) - separation;
|
||||
|
||||
// First, we measure non-stretching children.
|
||||
foreach (var child in Children)
|
||||
@@ -76,46 +79,74 @@ namespace Robust.Client.UserInterface.Controls
|
||||
if (!child.Visible)
|
||||
continue;
|
||||
|
||||
child.Measure(availableSize);
|
||||
child.Measure(TAxis.SizeFromAxis(availableSize));
|
||||
var childDesired = TAxis.SizeToAxis(child.DesiredSize);
|
||||
|
||||
if (Vertical)
|
||||
{
|
||||
desiredSize.Y += child.DesiredSize.Y;
|
||||
desiredSize.X = Math.Max(desiredSize.X, child.DesiredSize.X);
|
||||
availableSize.Y = Math.Max(0, availableSize.Y - child.DesiredSize.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
desiredSize.X += child.DesiredSize.X;
|
||||
desiredSize.Y = Math.Max(desiredSize.Y, child.DesiredSize.Y);
|
||||
availableSize.X = Math.Max(0, availableSize.X - child.DesiredSize.X);
|
||||
}
|
||||
desiredSize.X += childDesired.X;
|
||||
desiredSize.Y = Math.Max(desiredSize.Y, childDesired.Y);
|
||||
|
||||
availableSize.X = Math.Max(0, availableSize.X - childDesired.X);
|
||||
}
|
||||
|
||||
return desiredSize;
|
||||
return TAxis.SizeFromAxis(desiredSize);
|
||||
}
|
||||
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
var separation = ActualSeparation;
|
||||
var visibleChildCount = Children.Where(c => c.Visible).Count();
|
||||
|
||||
var stretchAvail = Vertical ? finalSize.Y : finalSize.X;
|
||||
if (Orientation == LayoutOrientation.Vertical)
|
||||
{
|
||||
LayOutItems<VerticalAxis>(default, finalSize, Align, Children, 0, ChildCount, separation);
|
||||
}
|
||||
else
|
||||
{
|
||||
LayOutItems<HorizontalAxis>(default, finalSize, Align, Children, 0, ChildCount, separation);
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
internal static void LayOutItems<TAxis>(
|
||||
Vector2 baseOffset,
|
||||
Vector2 finalSize,
|
||||
AlignMode align,
|
||||
OrderedChildCollection children,
|
||||
int start,
|
||||
int end,
|
||||
float separation,
|
||||
Vector2? fixedSize = null)
|
||||
where TAxis : IAxisImplementation
|
||||
{
|
||||
var realFinalSize = finalSize;
|
||||
finalSize = TAxis.SizeToAxis(finalSize);
|
||||
fixedSize = fixedSize == null ? null : TAxis.SizeToAxis(fixedSize.Value);
|
||||
|
||||
var visibleChildCount = 0;
|
||||
for (var i = start; i < end; i++)
|
||||
{
|
||||
if (children[i].Visible)
|
||||
visibleChildCount += 1;
|
||||
}
|
||||
|
||||
var stretchAvail = finalSize.X;
|
||||
stretchAvail -= separation * (visibleChildCount - 1);
|
||||
stretchAvail = Math.Max(0, stretchAvail);
|
||||
|
||||
// Step one: figure out the sizes of all our children and whether they want to stretch.
|
||||
var sizeList = new List<(Control control, float size, bool stretch)>(visibleChildCount);
|
||||
var totalStretchRatio = 0f;
|
||||
foreach (var child in Children)
|
||||
for (var i = start; i < end; i++)
|
||||
{
|
||||
var child = children[i];
|
||||
if (!child.Visible)
|
||||
continue;
|
||||
|
||||
bool stretch = Vertical ? child.VerticalExpand : child.HorizontalExpand;
|
||||
bool stretch = TAxis.GetMainExpandFlag(child);
|
||||
if (!stretch)
|
||||
{
|
||||
var size = Vertical ? child.DesiredSize.Y : child.DesiredSize.X;
|
||||
var measuredSize = fixedSize ?? TAxis.SizeToAxis(child.DesiredSize);
|
||||
var size = measuredSize.X;
|
||||
size = Math.Clamp(size, 0, stretchAvail);
|
||||
stretchAvail -= size;
|
||||
sizeList.Add((child, size, false));
|
||||
@@ -146,7 +177,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
continue;
|
||||
|
||||
var share = stretchAvail * control.SizeFlagsStretchRatio / totalStretchRatio;
|
||||
var desired = Vertical ? control.DesiredSize.Y : control.DesiredSize.X;
|
||||
var measuredSize = fixedSize ?? TAxis.SizeToAxis(control.DesiredSize);
|
||||
var desired = measuredSize.X;
|
||||
if (share >= desired)
|
||||
{
|
||||
sizeList[i] = (control, share, true);
|
||||
@@ -164,7 +196,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
else
|
||||
{
|
||||
// No stretching children -> offset the children based on the alignment.
|
||||
switch (Align)
|
||||
switch (align)
|
||||
{
|
||||
case AlignMode.Begin:
|
||||
break;
|
||||
@@ -190,22 +222,14 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
first = false;
|
||||
|
||||
UIBox2 targetBox;
|
||||
if (Vertical)
|
||||
{
|
||||
targetBox = new UIBox2(0, offset, finalSize.X, offset + size);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetBox = new UIBox2(offset, 0, offset + size, finalSize.Y);
|
||||
}
|
||||
var targetBox = TAxis.BoxFromAxis(new UIBox2(offset, 0, offset + size, finalSize.Y), realFinalSize);
|
||||
|
||||
targetBox = targetBox.Translated(baseOffset);
|
||||
|
||||
control.Arrange(targetBox);
|
||||
|
||||
offset += size;
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
public enum AlignMode : byte
|
||||
|
||||
@@ -121,11 +121,14 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
if (show)
|
||||
{
|
||||
if (Root == null)
|
||||
throw new InvalidOperationException("No UI root! We can't pop up!");
|
||||
|
||||
var globalPos = GlobalPosition;
|
||||
_popupVBox.Measure(Vector2Helpers.Infinity);
|
||||
var (minX, minY) = _popupVBox.DesiredSize;
|
||||
var box = UIBox2.FromDimensions(globalPos, new Vector2(Math.Max(minX, Width), minY));
|
||||
UserInterfaceManager.ModalRoot.AddChild(_popup);
|
||||
Root.ModalRoot.AddChild(_popup);
|
||||
_popup.Open(box);
|
||||
}
|
||||
else
|
||||
@@ -136,7 +139,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
private void OnPopupHide()
|
||||
{
|
||||
UserInterfaceManager.ModalRoot.RemoveChild(_popup);
|
||||
_popup.Orphan();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -135,7 +135,13 @@ namespace Robust.Client.UserInterface.Controls
|
||||
ClydeWindow.Resized += OnWindowResized;
|
||||
|
||||
_root = UserInterfaceManager.CreateWindowRoot(ClydeWindow);
|
||||
_root.CreateRootControls();
|
||||
|
||||
// Add ourselves *after* creating the root.
|
||||
// This way root controls are valid in EnteredTree().
|
||||
// We have to re-organize the controls after, of course.
|
||||
_root.AddChild(this);
|
||||
SetPositionFirst();
|
||||
|
||||
// Resize the window by our UIScale
|
||||
ClydeWindow.Size = new((int)(ClydeWindow.Size.X * UIScale), (int)(ClydeWindow.Size.Y * UIScale));
|
||||
|
||||
@@ -153,13 +153,16 @@ namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
if (show)
|
||||
{
|
||||
if (Root == null)
|
||||
throw new InvalidOperationException("No UI root! We can't pop up!");
|
||||
|
||||
var globalPos = GlobalPosition;
|
||||
globalPos.Y += Size.Y + 1; // Place it below us, with a safety margin.
|
||||
globalPos.Y -= Margin.SumVertical;
|
||||
OptionsScroll.Measure(Window?.Size ?? Vector2Helpers.Infinity);
|
||||
var (minX, minY) = OptionsScroll.DesiredSize;
|
||||
var box = UIBox2.FromDimensions(globalPos, new Vector2(Math.Max(minX, Width), minY));
|
||||
UserInterfaceManager.ModalRoot.AddChild(_popup);
|
||||
Root.ModalRoot.AddChild(_popup);
|
||||
_popup.Open(box);
|
||||
}
|
||||
else
|
||||
@@ -170,7 +173,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
private void OnPopupHide()
|
||||
{
|
||||
UserInterfaceManager.ModalRoot.RemoveChild(_popup);
|
||||
_popup.Orphan();
|
||||
}
|
||||
|
||||
private void ButtonOnPressed(ButtonEventArgs obj)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
@@ -154,6 +155,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
public void SetMessage(Index index, FormattedMessage message, Type[]? tagsAllowed = null, Color? defaultColor = null)
|
||||
{
|
||||
var atBottom = !_scrollDownButton.Visible;
|
||||
var oldEntry = _entries[index];
|
||||
var font = _getFont();
|
||||
_totalContentHeight -= oldEntry.Height + font.GetLineSeparation(UIScale);
|
||||
@@ -164,6 +166,10 @@ namespace Robust.Client.UserInterface.Controls
|
||||
_entries[index] = entry;
|
||||
|
||||
AddNewItemHeight(font, in entry);
|
||||
|
||||
_scrollBar.MaxValue = Math.Max(_scrollBar.Page, _totalContentHeight);
|
||||
if (atBottom)
|
||||
_scrollBar.Value = _scrollBar.MaxValue;
|
||||
}
|
||||
|
||||
private void AddNewItemHeight(Font font, in RichTextEntry entry)
|
||||
@@ -278,7 +284,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.Contracts.Pure]
|
||||
[Pure]
|
||||
private Font _getFont()
|
||||
{
|
||||
if (TryGetStyleProperty<Font>("font", out var font))
|
||||
@@ -289,7 +295,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return UserInterfaceManager.ThemeDefaults.DefaultFont;
|
||||
}
|
||||
|
||||
[System.Diagnostics.Contracts.Pure]
|
||||
[Pure]
|
||||
private StyleBox? _getStyleBox()
|
||||
{
|
||||
if (StyleBoxOverride != null)
|
||||
@@ -301,14 +307,14 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return box;
|
||||
}
|
||||
|
||||
[System.Diagnostics.Contracts.Pure]
|
||||
[Pure]
|
||||
private float _getScrollSpeed()
|
||||
{
|
||||
// The scroll speed depends on the UI scale because the scroll bar is working with physical pixels.
|
||||
return GetScrollSpeed(_getFont(), UIScale);
|
||||
}
|
||||
|
||||
[System.Diagnostics.Contracts.Pure]
|
||||
[Pure]
|
||||
private UIBox2 _getContentBox()
|
||||
{
|
||||
var style = _getStyleBox();
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
public bool CloseOnEscape { get; set; } = true;
|
||||
|
||||
public virtual void Open(UIBox2? box = null, Vector2? altPos = null)
|
||||
public virtual void Open(UIBox2? box = null, Vector2? altPos = null, Vector2? altPosUp = null)
|
||||
{
|
||||
if (Visible)
|
||||
{
|
||||
@@ -44,10 +44,12 @@ namespace Robust.Client.UserInterface.Controls
|
||||
if (box != null &&
|
||||
(_desiredSize != box.Value.Size ||
|
||||
PopupContainer.GetPopupOrigin(this) != box.Value.TopLeft ||
|
||||
PopupContainer.GetAltOrigin(this) != altPos))
|
||||
PopupContainer.GetAltOrigin(this) != altPos ||
|
||||
PopupContainer.GetAltOriginUp(this) != altPosUp))
|
||||
{
|
||||
PopupContainer.SetPopupOrigin(this, box.Value.TopLeft);
|
||||
PopupContainer.SetAltOrigin(this, altPos);
|
||||
PopupContainer.SetAltOriginUp(this, altPosUp);
|
||||
|
||||
_desiredSize = box.Value.Size;
|
||||
InvalidateMeasure();
|
||||
|
||||
@@ -27,6 +27,13 @@ namespace Robust.Client.UserInterface.Controls
|
||||
public static readonly AttachedProperty AltOriginProperty = AttachedProperty.Create("AltOrigin",
|
||||
typeof(PopupContainer), typeof(Vector2?), changed: PopupOriginChangedCallback);
|
||||
|
||||
/// <summary>
|
||||
/// Alternative position to bottom-left-align the popup if <see cref="PopupOriginProperty"/>
|
||||
/// would put it off-screen vertically.
|
||||
/// </summary>
|
||||
public static readonly AttachedProperty AltOriginUpProperty = AttachedProperty.Create("AltOriginUp",
|
||||
typeof(PopupContainer), typeof(Vector2?), changed: PopupOriginChangedCallback);
|
||||
|
||||
public PopupContainer()
|
||||
{
|
||||
RectClipContent = true;
|
||||
@@ -47,11 +54,21 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return control.GetValue<Vector2?>(AltOriginProperty);
|
||||
}
|
||||
|
||||
public static Vector2? GetAltOriginUp(Control control)
|
||||
{
|
||||
return control.GetValue<Vector2?>(AltOriginUpProperty);
|
||||
}
|
||||
|
||||
public static void SetAltOrigin(Control control, Vector2? origin)
|
||||
{
|
||||
control.SetValue(AltOriginProperty, origin);
|
||||
}
|
||||
|
||||
public static void SetAltOriginUp(Control control, Vector2? origin)
|
||||
{
|
||||
control.SetValue(AltOriginUpProperty, origin);
|
||||
}
|
||||
|
||||
private static void PopupOriginChangedCallback(Control owner, AttachedPropertyChangedEventArgs eventArgs)
|
||||
{
|
||||
if (owner.Parent is PopupContainer container)
|
||||
@@ -67,47 +84,58 @@ namespace Robust.Client.UserInterface.Controls
|
||||
var size = child.DesiredSize;
|
||||
var offset = child.GetValue<Vector2>(PopupOriginProperty);
|
||||
var altPos = child.GetValue<Vector2?>(AltOriginProperty);
|
||||
var altPosUp = child.GetValue<Vector2?>(AltOriginUpProperty);
|
||||
|
||||
var (r, b) = size + offset; // bottom right corner.
|
||||
var box = UIBox2.FromDimensions(offset, size);
|
||||
|
||||
var isAltPos = false;
|
||||
var isAltPosUp = false;
|
||||
|
||||
// Clamp the right edge.
|
||||
if (r > Width)
|
||||
if (box.Right > Width)
|
||||
{
|
||||
// Try to position at alt pos.
|
||||
if (altPos != null && altPos.Value.X - size.X > 0)
|
||||
{
|
||||
// There is horizontal room at the alt pos so there we go.
|
||||
isAltPos = true;
|
||||
offset = new Vector2(altPos.Value.X - size.X, altPos.Value.Y);
|
||||
(_, b) = size + offset;
|
||||
box = UIBox2.FromDimensions(new Vector2(altPos.Value.X - size.X, altPos.Value.Y), size);
|
||||
}
|
||||
else
|
||||
{
|
||||
offset -= new Vector2(r - Width, 0);
|
||||
box = box.Translated(new Vector2(-(box.Right - Width), 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp the bottom edge.
|
||||
if (b > Height)
|
||||
if (box.Bottom > Height)
|
||||
{
|
||||
offset -= new Vector2(0, b - Height);
|
||||
// Try to position at alt pos.
|
||||
if (altPosUp != null && altPosUp.Value.Y - size.Y > 0)
|
||||
{
|
||||
// There is vertical room at the alt pos so there we go.
|
||||
isAltPosUp = true;
|
||||
box = UIBox2.FromDimensions(new Vector2(altPosUp.Value.X, altPosUp.Value.Y - size.Y), size);
|
||||
}
|
||||
else
|
||||
{
|
||||
box = box.Translated(new Vector2(0, -(box.Bottom - Height)));
|
||||
}
|
||||
}
|
||||
|
||||
// Try to clamp the left edge.
|
||||
if (offset.X < 0 && !isAltPos)
|
||||
if (box.Left < 0 && !isAltPos)
|
||||
{
|
||||
offset -= new Vector2(offset.X, 0);
|
||||
box = box.Translated(new Vector2(-offset.X, 0));
|
||||
}
|
||||
|
||||
// Try to clamp the top edge.
|
||||
if (offset.Y < 0)
|
||||
if (box.Top < 0 && !isAltPosUp)
|
||||
{
|
||||
offset -= new Vector2(0, offset.Y);
|
||||
box = box.Translated(new Vector2(0, -offset.Y));
|
||||
}
|
||||
|
||||
child.Arrange(UIBox2.FromDimensions(offset, size));
|
||||
child.Arrange(box);
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
else
|
||||
{
|
||||
_updating = true;
|
||||
Value = MathHelper.Lerp(Value, ValueTarget, Math.Min(args.DeltaSeconds * 15, 1));
|
||||
Value = UIAnimations.LerpAnimate(Value, ValueTarget, args.DeltaSeconds, 15);
|
||||
_updating = false;
|
||||
}
|
||||
}
|
||||
|
||||
270
Robust.Client/UserInterface/Controls/TableContainer.cs
Normal file
270
Robust.Client/UserInterface/Controls/TableContainer.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controls;
|
||||
|
||||
internal sealed class TableContainer : Container
|
||||
{
|
||||
private int _columns = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The absolute minimum width a column can be forced to.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If a column *asks* for less width than this (small contents), it can still be smaller.
|
||||
/// But if it asks for more it cannot go below this width.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public float MinForcedColumnWidth { get; set; } = 50;
|
||||
|
||||
// Scratch space used while calculating layout, cached to avoid regular allocations during layout pass.
|
||||
private ColumnData[] _columnDataCache = [];
|
||||
private RowData[] _rowDataCache = [];
|
||||
|
||||
/// <summary>
|
||||
/// How many columns should be displayed.
|
||||
/// </summary>
|
||||
public int Columns
|
||||
{
|
||||
get => _columns;
|
||||
set
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(value, 1, nameof(value));
|
||||
|
||||
_columns = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
ResetCachedArrays();
|
||||
|
||||
// Do a first pass measuring all child controls as if they're given infinite space.
|
||||
// This gives us a maximum width the columns want, which we use to proportion them later.
|
||||
var columnIdx = 0;
|
||||
foreach (var child in Children)
|
||||
{
|
||||
ref var column = ref _columnDataCache[columnIdx];
|
||||
|
||||
child.Measure(new Vector2(float.PositiveInfinity, float.PositiveInfinity));
|
||||
column.MaxWidth = Math.Max(column.MaxWidth, child.DesiredSize.X);
|
||||
|
||||
columnIdx += 1;
|
||||
if (columnIdx == _columns)
|
||||
columnIdx = 0;
|
||||
}
|
||||
|
||||
// Calculate Slack and MinWidth for all columns. Also calculate sums for all columns.
|
||||
var totalMinWidth = 0f;
|
||||
var totalMaxWidth = 0f;
|
||||
var totalSlack = 0f;
|
||||
|
||||
for (var c = 0; c < _columns; c++)
|
||||
{
|
||||
ref var column = ref _columnDataCache[c];
|
||||
column.MinWidth = Math.Min(column.MaxWidth, MinForcedColumnWidth);
|
||||
column.Slack = column.MaxWidth - column.MinWidth;
|
||||
|
||||
totalMinWidth += column.MinWidth;
|
||||
totalMaxWidth += column.MaxWidth;
|
||||
totalSlack += column.Slack;
|
||||
}
|
||||
|
||||
if (totalMaxWidth <= availableSize.X)
|
||||
{
|
||||
// We want less horizontal space than we're given. Huh, that's convenient.
|
||||
// Just set assigned width to be however much they asked for.
|
||||
// We could probably skip the second measure pass in this scenario,
|
||||
// but that's just an optimization, so I don't care right now.
|
||||
//
|
||||
// There's probably a very clever way to make this behavior work with the else block of logic,
|
||||
// just by fiddling with the math.
|
||||
// I'm dumb, it's 4:30 AM. Yeah, I *started* at 2 AM.
|
||||
for (var c = 0; c < _columns; c++)
|
||||
{
|
||||
ref var column = ref _columnDataCache[c];
|
||||
|
||||
column.AssignedWidth = column.MaxWidth;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// We don't have enough horizontal space,
|
||||
// at least without causing *some* sort of word wrapping (assuming text contents).
|
||||
//
|
||||
// Assign horizontal space proportional to the wanted maximum size of the columns.
|
||||
var assignableWidth = Math.Max(0, availableSize.X - totalMinWidth);
|
||||
for (var c = 0; c < _columns; c++)
|
||||
{
|
||||
ref var column = ref _columnDataCache[c];
|
||||
|
||||
var slackRatio = column.Slack / totalSlack;
|
||||
column.AssignedWidth = column.MinWidth + slackRatio * assignableWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// Go over controls for a second measuring pass, this time giving them their assigned measure width.
|
||||
// This will give us a height to slot into per-row data.
|
||||
// We still measure assuming infinite vertical space.
|
||||
// This control can't properly handle being constrained on the Y axis.
|
||||
columnIdx = 0;
|
||||
var rowIdx = 0;
|
||||
foreach (var child in Children)
|
||||
{
|
||||
ref var column = ref _columnDataCache[columnIdx];
|
||||
ref var row = ref _rowDataCache[rowIdx];
|
||||
|
||||
child.Measure(new Vector2(column.AssignedWidth, float.PositiveInfinity));
|
||||
row.MeasuredHeight = Math.Max(row.MeasuredHeight, child.DesiredSize.Y);
|
||||
|
||||
columnIdx += 1;
|
||||
if (columnIdx == _columns)
|
||||
{
|
||||
columnIdx = 0;
|
||||
rowIdx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Sum up height of all rows to get final measured table height.
|
||||
var totalHeight = 0f;
|
||||
for (var r = 0; r < _rowDataCache.Length; r++)
|
||||
{
|
||||
ref var row = ref _rowDataCache[r];
|
||||
totalHeight += row.MeasuredHeight;
|
||||
}
|
||||
|
||||
return new Vector2(Math.Min(availableSize.X, totalMaxWidth), totalHeight);
|
||||
}
|
||||
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
// TODO: Expand to fit given vertical space.
|
||||
|
||||
// Calculate MinWidth and Slack sums again from column data.
|
||||
// We could've cached these from measure but whatever.
|
||||
var totalMinWidth = 0f;
|
||||
var totalSlack = 0f;
|
||||
|
||||
for (var c = 0; c < _columns; c++)
|
||||
{
|
||||
ref var column = ref _columnDataCache[c];
|
||||
totalMinWidth += column.MinWidth;
|
||||
totalSlack += column.Slack;
|
||||
}
|
||||
|
||||
// Calculate new width based on final given size, also assign horizontal positions of all columns.
|
||||
var assignableWidth = Math.Max(0, finalSize.X - totalMinWidth);
|
||||
var xPos = 0f;
|
||||
for (var c = 0; c < _columns; c++)
|
||||
{
|
||||
ref var column = ref _columnDataCache[c];
|
||||
|
||||
var slackRatio = column.Slack / totalSlack;
|
||||
column.ArrangedWidth = column.MinWidth + slackRatio * assignableWidth;
|
||||
column.ArrangedX = xPos;
|
||||
|
||||
xPos += column.ArrangedWidth;
|
||||
}
|
||||
|
||||
// Do actual arrangement row-by-row.
|
||||
var arrangeY = 0f;
|
||||
for (var r = 0; r < _rowDataCache.Length; r++)
|
||||
{
|
||||
ref var row = ref _rowDataCache[r];
|
||||
|
||||
for (var c = 0; c < _columns; c++)
|
||||
{
|
||||
ref var column = ref _columnDataCache[c];
|
||||
var index = c + r * _columns;
|
||||
|
||||
if (index >= ChildCount) // Quit early if we don't actually fill out the row.
|
||||
break;
|
||||
var child = GetChild(c + r * _columns);
|
||||
|
||||
child.Arrange(UIBox2.FromDimensions(column.ArrangedX, arrangeY, column.ArrangedWidth, row.MeasuredHeight));
|
||||
}
|
||||
|
||||
arrangeY += row.MeasuredHeight;
|
||||
}
|
||||
|
||||
return finalSize with { Y = arrangeY };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure cached array space is allocated to correct size and is reset to a clean slate.
|
||||
/// </summary>
|
||||
private void ResetCachedArrays()
|
||||
{
|
||||
// 1-argument Array.Clear() is not currently available in sandbox (added in .NET 6).
|
||||
|
||||
if (_columnDataCache.Length != _columns)
|
||||
_columnDataCache = new ColumnData[_columns];
|
||||
|
||||
Array.Clear(_columnDataCache, 0, _columnDataCache.Length);
|
||||
|
||||
var rowCount = ChildCount / _columns;
|
||||
if (ChildCount % _columns != 0)
|
||||
rowCount += 1;
|
||||
|
||||
if (rowCount != _rowDataCache.Length)
|
||||
_rowDataCache = new RowData[rowCount];
|
||||
|
||||
Array.Clear(_rowDataCache, 0, _rowDataCache.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-column data used during layout.
|
||||
/// </summary>
|
||||
private struct ColumnData
|
||||
{
|
||||
// Measure data.
|
||||
|
||||
/// <summary>
|
||||
/// The maximum width any control in this column wants, if given infinite space.
|
||||
/// Maximum of all controls on the column.
|
||||
/// </summary>
|
||||
public float MaxWidth;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum width this column may be given.
|
||||
/// This is either <see cref="MaxWidth"/> or <see cref="TableContainer.MinForcedColumnWidth"/>.
|
||||
/// </summary>
|
||||
public float MinWidth;
|
||||
|
||||
/// <summary>
|
||||
/// Difference between max and min width; how much this column can expand from its minimum.
|
||||
/// </summary>
|
||||
public float Slack;
|
||||
|
||||
/// <summary>
|
||||
/// How much horizontal space this column was assigned at measure time.
|
||||
/// </summary>
|
||||
public float AssignedWidth;
|
||||
|
||||
// Arrange data.
|
||||
|
||||
/// <summary>
|
||||
/// How much horizontal space this column was assigned at arrange time.
|
||||
/// </summary>
|
||||
public float ArrangedWidth;
|
||||
|
||||
/// <summary>
|
||||
/// The horizontal position this column was assigned at arrange time.
|
||||
/// </summary>
|
||||
public float ArrangedX;
|
||||
}
|
||||
|
||||
private struct RowData
|
||||
{
|
||||
// Measure data.
|
||||
|
||||
/// <summary>
|
||||
/// How much height the tallest control on this row was measured at,
|
||||
/// measuring for infinite vertical space but assigned column width.
|
||||
/// </summary>
|
||||
public float MeasuredHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
public Color? BackgroundColor { get; set; }
|
||||
|
||||
public virtual LayoutContainer PopupRoot => throw new NotSupportedException();
|
||||
public virtual PopupContainer ModalRoot => throw new NotSupportedException();
|
||||
|
||||
private Color _styleBgColor;
|
||||
|
||||
internal Color ActualBgColor => BackgroundColor ?? _styleBgColor;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
using Robust.Client.Graphics;
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controls
|
||||
{
|
||||
public sealed class WindowRoot : UIRoot
|
||||
{
|
||||
private PopupContainer? _modalRoot;
|
||||
private LayoutContainer? _popupRoot;
|
||||
|
||||
internal WindowRoot(IClydeWindow window)
|
||||
{
|
||||
Window = window;
|
||||
@@ -32,5 +34,39 @@ namespace Robust.Client.UserInterface.Controls
|
||||
/// </remarks>
|
||||
/// <seealso cref="CVars.ResAutoScaleEnabled"/>
|
||||
public bool DisableAutoScaling { get; set; } = true;
|
||||
|
||||
public override PopupContainer ModalRoot => _modalRoot ?? throw new InvalidOperationException(
|
||||
$"Tried to access root controls without calling {nameof(CreateRootControls)}!");
|
||||
|
||||
public override LayoutContainer PopupRoot => _popupRoot ?? throw new InvalidOperationException(
|
||||
$"Tried to access root controls without calling {nameof(CreateRootControls)}!");
|
||||
|
||||
/// <summary>
|
||||
/// Creates root controls (e.g. <see cref="UIRoot.ModalRoot"/>) that are necessary for the UI system to
|
||||
/// fully function.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should be called *after* inserting the main content into this instance,
|
||||
/// so that the created root controls (e.g. popups) correctly stay on top.
|
||||
/// </remarks>
|
||||
public void CreateRootControls()
|
||||
{
|
||||
if (_modalRoot != null)
|
||||
throw new InvalidOperationException("We've already created root controls!");
|
||||
|
||||
_modalRoot = new PopupContainer
|
||||
{
|
||||
Name = nameof(ModalRoot),
|
||||
MouseFilter = MouseFilterMode.Ignore,
|
||||
};
|
||||
AddChild(_modalRoot);
|
||||
|
||||
_popupRoot = new LayoutContainer
|
||||
{
|
||||
Name = nameof(PopupRoot),
|
||||
MouseFilter = MouseFilterMode.Ignore
|
||||
};
|
||||
AddChild(_popupRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
330
Robust.Client/UserInterface/Controls/WrapContainer.cs
Normal file
330
Robust.Client/UserInterface/Controls/WrapContainer.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.UserInterface.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Lays of children sequentially, "wrapping" them onto another row/column if necessary.
|
||||
/// </summary>
|
||||
public sealed class WrapContainer : Container
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the amount of space between two children, on the main axis.
|
||||
/// </summary>
|
||||
public const string StylePropertySeparation = "separation";
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the amount of space between two children, on the cross axis.
|
||||
/// </summary>
|
||||
public const string StylePropertyCrossSeparation = "cross-separation";
|
||||
|
||||
// Parameters.
|
||||
private Axis _layoutAxis;
|
||||
private ItemJustification _justification;
|
||||
private bool _equalSize;
|
||||
private bool _reverse;
|
||||
private int? _separationOverride;
|
||||
private int? _crossSeparationOverride;
|
||||
|
||||
// Cached layout data.
|
||||
private ValueList<(int endIndex, float cross)> _rowIndices;
|
||||
private float _lastMeasureCross;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the amount of space between two children, on the main axis.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property overrides <see cref="StylePropertySeparation"/>, if set.
|
||||
/// </remarks>
|
||||
public int? SeparationOverride
|
||||
{
|
||||
get => _separationOverride;
|
||||
set
|
||||
{
|
||||
_separationOverride = value;
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the amount of space between two children, on the cross axis.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This property overrides <see cref="StylePropertyCrossSeparation"/>, if set.
|
||||
/// </remarks>
|
||||
public int? CrossSeparationOverride
|
||||
{
|
||||
get => _crossSeparationOverride;
|
||||
set
|
||||
{
|
||||
_crossSeparationOverride = value;
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Axis"/> along which to lay out children.
|
||||
/// </summary>
|
||||
public Axis LayoutAxis
|
||||
{
|
||||
get => _layoutAxis;
|
||||
set
|
||||
{
|
||||
_layoutAxis = value;
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If true, all children will be laid out with the size of the largest child.
|
||||
/// </summary>
|
||||
public bool EqualSize
|
||||
{
|
||||
get => _equalSize;
|
||||
set
|
||||
{
|
||||
_equalSize = value;
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines where items will be laid out on an individual row/column.
|
||||
/// </summary>
|
||||
public ItemJustification Justification
|
||||
{
|
||||
get => _justification;
|
||||
set
|
||||
{
|
||||
_justification = value;
|
||||
InvalidateArrange();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If true, reverses the order on which wrapping rows/columns are laid out.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// On horizontal axis, the first children are on the top row, and new rows are added <i>downwards</i>.
|
||||
/// With <see cref="Reverse"/> set to true,
|
||||
/// the first children are instead on the bottom row, with new rows growing upwards.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public bool Reverse
|
||||
{
|
||||
get => _reverse;
|
||||
set
|
||||
{
|
||||
_reverse = value;
|
||||
InvalidateArrange();
|
||||
}
|
||||
}
|
||||
|
||||
private int ActualSeparation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TryGetStyleProperty(StylePropertySeparation, out int separation))
|
||||
{
|
||||
return separation;
|
||||
}
|
||||
|
||||
return SeparationOverride ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
private int ActualCrossSeparation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TryGetStyleProperty(StylePropertyCrossSeparation, out int separation))
|
||||
{
|
||||
return separation;
|
||||
}
|
||||
|
||||
return CrossSeparationOverride ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
var axis = LayoutAxis;
|
||||
|
||||
return axis switch
|
||||
{
|
||||
Axis.Horizontal => MeasureImplementation<HorizontalAxis>(availableSize),
|
||||
Axis.HorizontalReverse => MeasureImplementation<HorizontalReverseAxis>(availableSize),
|
||||
Axis.Vertical => MeasureImplementation<VerticalAxis>(availableSize),
|
||||
Axis.VerticalReverse => MeasureImplementation<VerticalReverseAxis>(availableSize),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
private Vector2 MeasureImplementation<TAxis>(Vector2 availableSize) where TAxis : IAxisImplementation
|
||||
{
|
||||
_rowIndices.Clear();
|
||||
|
||||
var realAvailableSize = availableSize;
|
||||
availableSize = TAxis.SizeToAxis(availableSize);
|
||||
|
||||
// TODO: Round to pixels properly.
|
||||
var separation = ActualSeparation;
|
||||
var crossSeparation = ActualCrossSeparation;
|
||||
var curMainSize = 0f;
|
||||
var curCrossSize = 0f;
|
||||
var totalCrossSize = 0f;
|
||||
var maxMainSize = 0f;
|
||||
var firstOnRow = true;
|
||||
|
||||
var equalDesiredSize = _equalSize ? TAxis.SizeToAxis(GetMaxMeasure(realAvailableSize)) : default;
|
||||
var countLaidOut = 0;
|
||||
|
||||
foreach (var control in Children)
|
||||
{
|
||||
Vector2 controlDesiredSize;
|
||||
if (_equalSize)
|
||||
{
|
||||
controlDesiredSize = equalDesiredSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
control.Measure(realAvailableSize);
|
||||
controlDesiredSize = TAxis.SizeToAxis(control.DesiredSize);
|
||||
}
|
||||
|
||||
var controlMainSize = controlDesiredSize.X;
|
||||
var spaceTaken = controlMainSize + (firstOnRow ? 0 : separation);
|
||||
if (curMainSize + spaceTaken > availableSize.X)
|
||||
{
|
||||
// We've wrapped.
|
||||
RowEnd(lastRow: false);
|
||||
curMainSize = controlMainSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
curMainSize += spaceTaken;
|
||||
}
|
||||
curCrossSize = Math.Max(curCrossSize, controlDesiredSize.Y);
|
||||
firstOnRow = false;
|
||||
countLaidOut += 1;
|
||||
}
|
||||
|
||||
RowEnd(lastRow: true);
|
||||
|
||||
_lastMeasureCross = totalCrossSize;
|
||||
|
||||
return TAxis.SizeFromAxis(new Vector2(maxMainSize, totalCrossSize));
|
||||
|
||||
void RowEnd(bool lastRow)
|
||||
{
|
||||
maxMainSize = Math.Max(maxMainSize, curMainSize);
|
||||
totalCrossSize += curCrossSize;
|
||||
if (!lastRow)
|
||||
totalCrossSize += crossSeparation;
|
||||
_rowIndices.Add((countLaidOut, curCrossSize));
|
||||
curCrossSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 GetMaxDesired()
|
||||
{
|
||||
var vec = Vector2.Zero;
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
vec = Vector2.Max(vec, child.DesiredSize);
|
||||
}
|
||||
|
||||
return vec;
|
||||
}
|
||||
|
||||
private Vector2 GetMaxMeasure(Vector2 availableSize)
|
||||
{
|
||||
var vec = Vector2.Zero;
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
child.Measure(availableSize);
|
||||
vec = Vector2.Max(vec, child.DesiredSize);
|
||||
}
|
||||
|
||||
return vec;
|
||||
}
|
||||
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
var axis = LayoutAxis;
|
||||
|
||||
return axis switch
|
||||
{
|
||||
Axis.Horizontal => ArrangeImplementation<HorizontalAxis>(finalSize),
|
||||
Axis.HorizontalReverse => ArrangeImplementation<HorizontalReverseAxis>(finalSize),
|
||||
Axis.Vertical => ArrangeImplementation<VerticalAxis>(finalSize),
|
||||
Axis.VerticalReverse => ArrangeImplementation<VerticalReverseAxis>(finalSize),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
|
||||
private Vector2 ArrangeImplementation<TAxis>(Vector2 finalSize) where TAxis : IAxisImplementation
|
||||
{
|
||||
var realFinalSize = finalSize;
|
||||
finalSize = TAxis.SizeToAxis(finalSize);
|
||||
|
||||
var separation = ActualSeparation;
|
||||
var crossSeparation = ActualCrossSeparation;
|
||||
|
||||
var baseOffset = Reverse ? new Vector2(0, _lastMeasureCross) : Vector2.Zero;
|
||||
|
||||
var fixedSize = _equalSize ? (Vector2?)GetMaxDesired() : null;
|
||||
|
||||
var start = 0;
|
||||
for (var i = 0; i < _rowIndices.Count; i++)
|
||||
{
|
||||
var (endIndex, cross) = _rowIndices[i];
|
||||
|
||||
if (Reverse)
|
||||
{
|
||||
baseOffset.Y -= cross;
|
||||
}
|
||||
|
||||
var box = TAxis.BoxFromAxis(UIBox2.FromDimensions(baseOffset, finalSize with { Y = cross }), realFinalSize);
|
||||
|
||||
BoxContainer.LayOutItems<TAxis>(
|
||||
box.TopLeft,
|
||||
box.Size,
|
||||
(BoxContainer.AlignMode)_justification,
|
||||
Children,
|
||||
start,
|
||||
endIndex,
|
||||
separation,
|
||||
fixedSize);
|
||||
|
||||
start = endIndex;
|
||||
|
||||
if (Reverse)
|
||||
{
|
||||
baseOffset.Y -= crossSeparation;
|
||||
}
|
||||
else
|
||||
{
|
||||
baseOffset.Y = baseOffset.Y + cross + crossSeparation;
|
||||
}
|
||||
}
|
||||
|
||||
return realFinalSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Justification values for <see cref="WrapContainer.Justification"/>.
|
||||
/// </summary>
|
||||
public enum ItemJustification : byte
|
||||
{
|
||||
// These MUST match the values in BoxContainer.
|
||||
Begin = BoxContainer.AlignMode.Begin,
|
||||
Center = BoxContainer.AlignMode.Center,
|
||||
End = BoxContainer.AlignMode.End
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared;
|
||||
@@ -224,10 +225,13 @@ public sealed partial class DebugConsole
|
||||
|
||||
if (_compPopup.Contents.ChildCount != 0)
|
||||
{
|
||||
_compPopup.Open(
|
||||
UIBox2.FromDimensions(
|
||||
offset - _compPopup.Contents.Margin.Left, CommandBar.GlobalPosition.Y + CommandBar.Height + 2,
|
||||
5, 5));
|
||||
var box = UIBox2.FromDimensions(
|
||||
offset - _compPopup.Contents.Margin.Left,
|
||||
CommandBar.GlobalPosition.Y + CommandBar.Height + 2,
|
||||
5,
|
||||
5);
|
||||
var altPosUp = new Vector2(offset - _compPopup.Contents.Margin.Left, CommandBar.GlobalPosition.Y);
|
||||
_compPopup.Open(box, altPosUp: altPosUp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
_consoleHost.ClearText += OnClearText;
|
||||
_cfg.OnValueChanged(CVars.ConMaxEntries, MaxEntriesChanged, true);
|
||||
|
||||
UserInterfaceManager.ModalRoot.AddChild(_compPopup);
|
||||
Root!.ModalRoot.AddChild(_compPopup);
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
@@ -95,7 +95,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
_consoleHost.ClearText -= OnClearText;
|
||||
_cfg.UnsubValueChanged(CVars.ConMaxEntries, MaxEntriesChanged);
|
||||
|
||||
UserInterfaceManager.ModalRoot.RemoveChild(_compPopup);
|
||||
_compPopup.Orphan();
|
||||
}
|
||||
|
||||
private void MaxEntriesChanged(int value)
|
||||
|
||||
@@ -1,42 +1,39 @@
|
||||
using System;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.Profiling;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
|
||||
{
|
||||
internal sealed class DebugMonitors : BoxContainer, IDebugMonitors
|
||||
{
|
||||
[Dependency] private readonly IClientGameTiming _timing = default!;
|
||||
[Dependency] private readonly IClientGameStateManager _state = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IClientNetManager _net = default!;
|
||||
|
||||
private readonly Control[] _monitors = new Control[Enum.GetNames<DebugMonitor>().Length];
|
||||
|
||||
//TODO: Think about a factory for this
|
||||
public DebugMonitors(IClientGameTiming gameTiming, IPlayerManager playerManager, IEyeManager eyeManager,
|
||||
IInputManager inputManager, IStateManager stateManager, IClyde displayManager, IClientNetManager netManager,
|
||||
IMapManager mapManager)
|
||||
public void Init()
|
||||
{
|
||||
Visible = false;
|
||||
|
||||
SeparationOverride = 2;
|
||||
Orientation = LayoutOrientation.Vertical;
|
||||
|
||||
Add(DebugMonitor.Fps, new FpsCounter(gameTiming));
|
||||
Add(DebugMonitor.Fps, new FpsCounter(_timing));
|
||||
Add(DebugMonitor.Coords, new DebugCoordsPanel());
|
||||
Add(DebugMonitor.Net, new DebugNetPanel(netManager, gameTiming));
|
||||
Add(DebugMonitor.Bandwidth, new DebugNetBandwidthPanel(netManager, gameTiming));
|
||||
Add(DebugMonitor.Time, new DebugTimePanel(gameTiming, IoCManager.Resolve<IClientGameStateManager>()));
|
||||
Add(DebugMonitor.Frames, new FrameGraph(gameTiming, IoCManager.Resolve<IConfigurationManager>()));
|
||||
Add(DebugMonitor.Net, new DebugNetPanel(_net, _timing));
|
||||
Add(DebugMonitor.Bandwidth, new DebugNetBandwidthPanel(_net, _timing));
|
||||
Add(DebugMonitor.Time, new DebugTimePanel(_timing, _state));
|
||||
Add(DebugMonitor.Frames, new FrameGraph(_timing, _cfg));
|
||||
Add(DebugMonitor.Memory, new DebugMemoryPanel());
|
||||
Add(DebugMonitor.Clyde, new DebugClydePanel { HorizontalAlignment = HAlignment.Left });
|
||||
Add(DebugMonitor.System, new DebugSystemPanel { HorizontalAlignment = HAlignment.Left });
|
||||
Add(DebugMonitor.Version, new DebugVersionPanel(_cfg) {HorizontalAlignment = HAlignment.Left});
|
||||
Add(DebugMonitor.Input, new DebugInputPanel { HorizontalAlignment = HAlignment.Left });
|
||||
Add(DebugMonitor.Prof, new LiveProfileViewControl());
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.UserInterface.CustomControls.DebugMonitorControls
|
||||
{
|
||||
internal sealed class DebugVersionPanel : PanelContainer
|
||||
{
|
||||
public DebugVersionPanel(IConfigurationManager cfg)
|
||||
{
|
||||
var contents = new Label
|
||||
{
|
||||
FontColorShadowOverride = Color.Black,
|
||||
};
|
||||
AddChild(contents);
|
||||
|
||||
PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = new Color(35, 134, 37, 138),
|
||||
ContentMarginLeftOverride = 5,
|
||||
ContentMarginRightOverride = 5,
|
||||
ContentMarginTopOverride = 5,
|
||||
ContentMarginBottomOverride = 5,
|
||||
};
|
||||
|
||||
MouseFilter = contents.MouseFilter = MouseFilterMode.Ignore;
|
||||
|
||||
// Set visible explicitly
|
||||
Visible = true;
|
||||
HorizontalAlignment = HAlignment.Left;
|
||||
VerticalAlignment = VAlignment.Top;
|
||||
|
||||
var buildInfo = GameBuildInformation.GetBuildInfoFromConfig(cfg);
|
||||
contents.Text = $"Fork ID: {buildInfo.ForkId}\nFork Version: {buildInfo.Version}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
}
|
||||
else
|
||||
{
|
||||
_curAnchorOffset = MathHelper.Lerp(_curAnchorOffset, targetOffset, args.DeltaSeconds * 20);
|
||||
_curAnchorOffset = UIAnimations.LerpAnimate(_curAnchorOffset, targetOffset, args.DeltaSeconds, 20);
|
||||
}
|
||||
|
||||
UpdateAnchorOffset();
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
<DevWindowTabUI Name="UI" />
|
||||
<DevWindowTabPerf Name="Perf" />
|
||||
<DevWindowTabTextures Name="Textures" />
|
||||
<DevWindowTabRenderTargets Name="RenderTargets" />
|
||||
</TabContainer>
|
||||
</Control>
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace Robust.Client.UserInterface
|
||||
TabContainer.SetTabTitle(UI, "User Interface");
|
||||
TabContainer.SetTabTitle(Perf, "Profiling");
|
||||
TabContainer.SetTabTitle(Textures, Loc.GetString("dev-window-tab-textures-title"));
|
||||
TabContainer.SetTabTitle(RenderTargets, Loc.GetString("dev-window-tab-render-targets-title"));
|
||||
|
||||
Stylesheet =
|
||||
new DefaultStylesheet(IoCManager.Resolve<IResourceCache>(), IoCManager.Resolve<IUserInterfaceManager>()).Stylesheet;
|
||||
@@ -41,26 +42,14 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var clyde = IoCManager.Resolve<IClyde>();
|
||||
var monitor = clyde.EnumerateMonitors().First();
|
||||
if (args.Length > 0)
|
||||
var window = new OSWindow
|
||||
{
|
||||
var id = int.Parse(args[0]);
|
||||
monitor = clyde.EnumerateMonitors().Single(m => m.Id == id);
|
||||
}
|
||||
|
||||
var window = clyde.CreateWindow(new WindowCreateParameters
|
||||
{
|
||||
//Maximized = true,
|
||||
Title = "Robust Debug Window",
|
||||
//Monitor = monitor,
|
||||
});
|
||||
var root = IoCManager.Resolve<IUserInterfaceManager>().CreateWindowRoot(window);
|
||||
window.DisposeOnClose = true;
|
||||
|
||||
};
|
||||
var control = new DevWindow();
|
||||
window.AddChild(control);
|
||||
|
||||
root.AddChild(control);
|
||||
window.Show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Robust.Client.UserInterface.DevWindowTabRenderTargets">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Button Text="{Loc 'dev-window-tab-render-targets-reload'}" Name="ReloadButton" />
|
||||
</BoxContainer>
|
||||
<LineEdit Name="FilterEdit" PlaceHolder="{Loc 'dev-window-tab-render-targets-filter'}" />
|
||||
<ScrollContainer VerticalExpand="True" VScrollEnabled="True" HScrollEnabled="False">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<TableContainer Name="Table" Margin="2" Columns="6">
|
||||
<Label Margin="4" Text="{Loc 'dev-window-tab-render-targets-column-id'}" />
|
||||
<Label Margin="4" Text="{Loc 'dev-window-tab-render-targets-column-name'}" />
|
||||
<Label Margin="4" Text="{Loc 'dev-window-tab-render-targets-column-size'}" />
|
||||
<Label Margin="4" Text="{Loc 'dev-window-tab-render-targets-column-type'}" />
|
||||
<Label Margin="4" Text="{Loc 'dev-window-tab-render-targets-column-vram'}" />
|
||||
<Label Margin="4" Text="{Loc 'dev-window-tab-render-targets-column-thumbnail'}" />
|
||||
</TableContainer>
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Name="TotalLabel" Margin="4" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
</Control>
|
||||
@@ -0,0 +1,242 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using RTCF = Robust.Client.Graphics.RenderTargetColorFormat;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
internal sealed partial class DevWindowTabRenderTargets : Control
|
||||
{
|
||||
[Dependency] private readonly IClydeInternal _clyde = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
|
||||
private readonly Control[] _gridHeader;
|
||||
|
||||
private readonly StyleBoxFlat _styleAltRow = new() { BackgroundColor = Color.FromHex("#222") };
|
||||
|
||||
#if TOOLS
|
||||
private readonly HashSet<IRenderTarget> _copyTextures = new();
|
||||
#endif // TOOLS
|
||||
|
||||
public DevWindowTabRenderTargets()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_gridHeader = Table.Children.ToArray();
|
||||
|
||||
ReloadButton.OnPressed += _ => Reload();
|
||||
FilterEdit.OnTextChanged += _ => Reload();
|
||||
}
|
||||
|
||||
protected override void VisibilityChanged(bool newVisible)
|
||||
{
|
||||
base.VisibilityChanged(newVisible);
|
||||
|
||||
if (newVisible)
|
||||
{
|
||||
Reload();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clear to release memory when tab not visible.
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
Clear();
|
||||
}
|
||||
|
||||
private void Clear()
|
||||
{
|
||||
Table.RemoveAllChildren();
|
||||
|
||||
#if TOOLS
|
||||
foreach (var copiedTexture in _copyTextures)
|
||||
{
|
||||
copiedTexture.Dispose();
|
||||
}
|
||||
|
||||
_copyTextures.Clear();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void Reload()
|
||||
{
|
||||
Table.RemoveAllChildren();
|
||||
|
||||
foreach (var header in _gridHeader)
|
||||
{
|
||||
Table.AddChild(header);
|
||||
}
|
||||
|
||||
var totalVram = 0L;
|
||||
|
||||
var even = true;
|
||||
|
||||
var rts = _clyde.GetLoadedRenderTextures().OrderBy(x => x.Item1.Handle.Value).ToArray();
|
||||
foreach (var (instance, loaded) in rts)
|
||||
{
|
||||
#if TOOLS
|
||||
if (_copyTextures.Contains(instance))
|
||||
continue;
|
||||
#endif // TOOLS
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(FilterEdit.Text))
|
||||
{
|
||||
if (loaded.Name is not { } name)
|
||||
continue;
|
||||
|
||||
if (!name.Contains(FilterEdit.Text, StringComparison.CurrentCultureIgnoreCase))
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
AddColumnText(instance.Handle.Value.ToString());
|
||||
|
||||
if (loaded.Name != null)
|
||||
AddColumnText(loaded.Name);
|
||||
else if (loaded.WindowId != WindowId.Invalid)
|
||||
AddColumnText(loaded.WindowId.ToString());
|
||||
else
|
||||
AddColumnText(_loc.GetString("dev-window-tab-render-targets-value-null"));
|
||||
|
||||
AddColumnText(loaded.Size.ToString());
|
||||
var type = loaded.ColorFormat.ToString();
|
||||
if (loaded.DepthStencilHandle != default)
|
||||
type += "+DS";
|
||||
AddColumnText(type);
|
||||
AddColumnText(ByteHelpers.FormatBytes(loaded.MemoryPressure));
|
||||
|
||||
// Disable texture thumbnails outside TOOLS.
|
||||
// Avoid people cheating by using devwindow to see through walls. Barely.
|
||||
#if TOOLS
|
||||
if (instance is IRenderTexture renderTexture)
|
||||
{
|
||||
var clone = CloneTexture(renderTexture.Texture, loaded.ColorFormat);
|
||||
|
||||
_copyTextures.Add(clone);
|
||||
|
||||
AddColumn(new TextureRect
|
||||
{
|
||||
Texture = clone.Texture,
|
||||
Stretch = TextureRect.StretchMode.KeepAspect
|
||||
});
|
||||
}
|
||||
else
|
||||
#endif // TOOLS
|
||||
{
|
||||
AddColumnText(_loc.GetString("dev-window-tab-render-targets-value-not-available"));
|
||||
}
|
||||
|
||||
totalVram += loaded.MemoryPressure;
|
||||
|
||||
even = !even;
|
||||
}
|
||||
|
||||
TotalLabel.Text = Loc.GetString(
|
||||
"dev-window-tab-render-targets-summary",
|
||||
("vram", ByteHelpers.FormatBytes(totalVram)));
|
||||
|
||||
return;
|
||||
|
||||
void AddColumnText(string text)
|
||||
{
|
||||
var richTextLabel = new RichTextLabel { Margin = new Thickness(4) };
|
||||
richTextLabel.SetMessage(text, defaultColor: Color.White);
|
||||
AddColumn(richTextLabel);
|
||||
}
|
||||
|
||||
void AddColumn(Control control)
|
||||
{
|
||||
control.VerticalAlignment = VAlignment.Center;
|
||||
|
||||
if (even)
|
||||
{
|
||||
control = new PanelContainer
|
||||
{
|
||||
PanelOverride = _styleAltRow,
|
||||
Children = { control },
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
// Wrapping control so we can use SetHeight.
|
||||
control = new Control
|
||||
{
|
||||
Children = { control },
|
||||
};
|
||||
}
|
||||
|
||||
control.SetHeight = 50;
|
||||
|
||||
Table.AddChild(control);
|
||||
}
|
||||
}
|
||||
|
||||
#if TOOLS
|
||||
private IRenderTexture CloneTexture(Texture texture, RTCF colorFormat)
|
||||
{
|
||||
var thumbnailSize = GetThumbnailSize(texture.Size);
|
||||
|
||||
var rt = _clyde.CreateRenderTarget(
|
||||
thumbnailSize,
|
||||
new RenderTargetFormatParameters
|
||||
{
|
||||
ColorFormat = colorFormat,
|
||||
HasDepthStencil = false,
|
||||
},
|
||||
new TextureSampleParameters
|
||||
{
|
||||
Filter = true,
|
||||
},
|
||||
name: $"{nameof(DevWindowTabRenderTargets)}-clone");
|
||||
|
||||
_clyde.RenderNow(rt,
|
||||
handle =>
|
||||
{
|
||||
handle.DrawingHandleScreen.DrawTextureRect(texture, UIBox2.FromDimensions(Vector2.Zero, thumbnailSize));
|
||||
});
|
||||
|
||||
return rt;
|
||||
}
|
||||
|
||||
private static Vector2i GetThumbnailSize(Vector2i textureSize)
|
||||
{
|
||||
const int maxHeight = 50;
|
||||
const int maxWidth = 100;
|
||||
|
||||
var (w, h) = (Vector2)textureSize;
|
||||
|
||||
if (h > maxHeight)
|
||||
{
|
||||
w /= h / maxHeight;
|
||||
h = maxHeight;
|
||||
}
|
||||
|
||||
if (w > maxWidth)
|
||||
{
|
||||
h /= w / maxWidth;
|
||||
w = maxWidth;
|
||||
}
|
||||
|
||||
return new Vector2i((int)w, (int)h);
|
||||
}
|
||||
#endif // TOOLS
|
||||
}
|
||||
@@ -36,5 +36,6 @@ public enum DebugMonitor
|
||||
Input,
|
||||
Bandwidth,
|
||||
Prof,
|
||||
System
|
||||
System,
|
||||
Version
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Robust.Client.UserInterface
|
||||
/// <param name="tooltip">control to position (current size will be used to determine bounds)</param>
|
||||
public static void PositionTooltip(Control tooltip)
|
||||
{
|
||||
PositionTooltip(tooltip.UserInterfaceManager.RootControl.Size,
|
||||
PositionTooltip(tooltip.Root!.Size,
|
||||
tooltip.UserInterfaceManager.MousePositionScaled.Position,
|
||||
tooltip);
|
||||
}
|
||||
|
||||
13
Robust.Client/UserInterface/UIAnimations.cs
Normal file
13
Robust.Client/UserInterface/UIAnimations.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Client.UserInterface;
|
||||
|
||||
internal static class UIAnimations
|
||||
{
|
||||
// From https://blog.pkh.me/p/41-fixing-the-iterative-damping-interpolation-in-video-games.html
|
||||
public static float LerpAnimate(float a, float b, float dt, float rate)
|
||||
{
|
||||
return MathHelper.Lerp(a, b, 1f - MathF.Exp(-dt * rate));
|
||||
}
|
||||
}
|
||||
@@ -333,7 +333,7 @@ internal partial class UserInterfaceManager
|
||||
|
||||
if (_suppliedTooltip != null)
|
||||
{
|
||||
PopupRoot.RemoveChild(_suppliedTooltip);
|
||||
_suppliedTooltip.Orphan();
|
||||
_suppliedTooltip = null;
|
||||
}
|
||||
|
||||
@@ -538,7 +538,7 @@ internal partial class UserInterfaceManager
|
||||
if (_showingTooltip) return;
|
||||
_showingTooltip = true;
|
||||
var hovered = CurrentlyHovered;
|
||||
if (hovered == null)
|
||||
if (hovered == null || hovered.Root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -563,7 +563,7 @@ internal partial class UserInterfaceManager
|
||||
if (_suppliedTooltip == null)
|
||||
return;
|
||||
|
||||
PopupRoot.AddChild(_suppliedTooltip);
|
||||
hovered.Root.PopupRoot.AddChild(_suppliedTooltip);
|
||||
Tooltips.PositionTooltip(_suppliedTooltip);
|
||||
hovered.PerformShowTooltip();
|
||||
}
|
||||
|
||||
@@ -3,10 +3,8 @@ using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.Timing;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.CustomControls.DebugMonitorControls;
|
||||
@@ -20,9 +18,7 @@ using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Profiling;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Reflection;
|
||||
@@ -37,13 +33,9 @@ namespace Robust.Client.UserInterface
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IFontManager _fontManager = default!;
|
||||
[Dependency] private readonly IClydeInternal _clyde = default!;
|
||||
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManagerInternal _userInterfaceManager = default!;
|
||||
[Dependency] private readonly IDynamicTypeFactoryInternal _typeFactory = default!;
|
||||
@@ -86,10 +78,10 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
[ViewVariables] public ViewportContainer MainViewport { get; private set; } = default!;
|
||||
[ViewVariables] public LayoutContainer StateRoot { get; private set; } = default!;
|
||||
[ViewVariables] public PopupContainer ModalRoot { get; private set; } = default!;
|
||||
[ViewVariables] public PopupContainer ModalRoot => RootControl.ModalRoot;
|
||||
[ViewVariables] public WindowRoot RootControl { get; private set; } = default!;
|
||||
[ViewVariables] public LayoutContainer WindowRoot { get; private set; } = default!;
|
||||
[ViewVariables] public LayoutContainer PopupRoot { get; private set; } = default!;
|
||||
[ViewVariables] public LayoutContainer PopupRoot => RootControl.PopupRoot;
|
||||
[ViewVariables] public DropDownDebugConsole DebugConsole { get; private set; } = default!;
|
||||
[ViewVariables] public IDebugMonitors DebugMonitors => _debugMonitors;
|
||||
private DebugMonitors _debugMonitors = default!;
|
||||
@@ -117,10 +109,11 @@ namespace Robust.Client.UserInterface
|
||||
|
||||
DebugConsole = new DropDownDebugConsole();
|
||||
RootControl.AddChild(DebugConsole);
|
||||
DebugConsole.SetPositionInParent(ModalRoot.GetPositionInParent());
|
||||
DebugConsole.SetPositionInParent(RootControl.ModalRoot.GetPositionInParent());
|
||||
|
||||
_debugMonitors = new DebugMonitors(_gameTiming, _playerManager, _eyeManager, _inputManager, _stateManager,
|
||||
_clyde, _netManager, _mapManager);
|
||||
_debugMonitors = new DebugMonitors();
|
||||
_rootDependencies.InjectDependencies(_debugMonitors);
|
||||
_debugMonitors.Init();
|
||||
DebugConsole.BelowConsole.AddChild(_debugMonitors);
|
||||
|
||||
_inputManager.SetInputCommand(EngineKeyFunctions.ShowDebugConsole,
|
||||
@@ -177,19 +170,7 @@ namespace Robust.Client.UserInterface
|
||||
};
|
||||
RootControl.AddChild(WindowRoot);
|
||||
|
||||
ModalRoot = new PopupContainer
|
||||
{
|
||||
Name = "ModalRoot",
|
||||
MouseFilter = Control.MouseFilterMode.Ignore,
|
||||
};
|
||||
RootControl.AddChild(ModalRoot);
|
||||
|
||||
PopupRoot = new LayoutContainer
|
||||
{
|
||||
Name = "PopupRoot",
|
||||
MouseFilter = Control.MouseFilterMode.Ignore
|
||||
};
|
||||
RootControl.AddChild(PopupRoot);
|
||||
RootControl.CreateRootControls();
|
||||
}
|
||||
|
||||
public void InitializeTesting()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Text;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -36,10 +36,18 @@ internal struct WordWrap
|
||||
LastRune = new Rune('A');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add <paramref name="rune" /> as the next glyph in the sequence and update internal line break state.
|
||||
/// </summary>
|
||||
/// <param name="breakLine">If non-null, indicates that a line break needs to be added at this rune
|
||||
/// index within the string. This index will refer to the offset of a previously-entered rune</param>
|
||||
/// <param name="breakNewLine">If non-null, indicates the rune index of an entered newline</param>
|
||||
/// <param name="skip">If true, indicates that the rune should occupy zero space. Currently only used
|
||||
/// for newlines.</param>
|
||||
public void NextRune(Rune rune, out int? breakLine, out int? breakNewLine, out bool skip)
|
||||
{
|
||||
BreakIndexCounter = NextBreakIndexCounter;
|
||||
NextBreakIndexCounter += rune.Utf16SequenceLength;
|
||||
NextBreakIndexCounter += 1;
|
||||
|
||||
breakLine = null;
|
||||
breakNewLine = null;
|
||||
|
||||
@@ -22,7 +22,7 @@ internal sealed class XamlHotReloadManager : IXamlHotReloadManager
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = null!;
|
||||
[Dependency] private readonly ILogManager _logManager = null!;
|
||||
[Dependency] private readonly IResourceManager _resources = null!;
|
||||
[Dependency] private readonly IResourceManagerInternal _resources = null!;
|
||||
[Dependency] private readonly ITaskManager _taskManager = null!;
|
||||
[Dependency] private readonly IXamlProxyManager _xamlProxyManager = null!;
|
||||
|
||||
@@ -120,9 +120,9 @@ internal sealed class XamlHotReloadManager : IXamlHotReloadManager
|
||||
private string? InferCodeLocation()
|
||||
{
|
||||
// ascend upwards from each content root until the solution file is found
|
||||
foreach (var contentRoot in _resources.GetContentRoots())
|
||||
foreach (var baseSystemPath in _resources.GetContentRoots())
|
||||
{
|
||||
var systemPath = contentRoot.ToRelativeSystemPath();
|
||||
var systemPath = baseSystemPath;
|
||||
while (true)
|
||||
{
|
||||
var files = Array.Empty<string>();
|
||||
|
||||
@@ -30,15 +30,6 @@ namespace Robust.Client.Utility
|
||||
#endif
|
||||
}
|
||||
|
||||
if (name == SDL.nativeLibName)
|
||||
{
|
||||
#if LINUX || FREEBSD
|
||||
return NativeLibrary.Load("libSDL3.so.0", assembly, path);
|
||||
#elif MACOS
|
||||
return NativeLibrary.Load("libSDL3.0.dylib", assembly, path);
|
||||
#endif
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ internal sealed class ReloadManager : IReloadManager
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ILogManager _logMan = default!;
|
||||
[Dependency] private readonly IResourceManager _res = default!;
|
||||
[Dependency] private readonly IResourceManagerInternal _res = default!;
|
||||
#pragma warning disable CS0414
|
||||
[Dependency] private readonly ITaskManager _tasks = default!;
|
||||
#pragma warning restore CS0414
|
||||
@@ -72,7 +72,7 @@ internal sealed class ReloadManager : IReloadManager
|
||||
|
||||
public void Register(ResPath directory, string filter)
|
||||
{
|
||||
Register(directory.ToString(), filter);
|
||||
Register(directory.ToRelativeSystemPath(), filter);
|
||||
}
|
||||
|
||||
public void Register(string directory, string filter)
|
||||
@@ -83,7 +83,7 @@ internal sealed class ReloadManager : IReloadManager
|
||||
#if TOOLS
|
||||
foreach (var root in _res.GetContentRoots())
|
||||
{
|
||||
var path = root + directory;
|
||||
var path = Path.Join(root, directory);
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
@@ -128,17 +128,17 @@ internal sealed class ReloadManager : IReloadManager
|
||||
|
||||
_tasks.RunOnMainThread(() =>
|
||||
{
|
||||
var fullPath = args.FullPath.Replace(Path.DirectorySeparatorChar, '/');
|
||||
var file = new ResPath(fullPath);
|
||||
|
||||
foreach (var rootIter in _res.GetContentRoots())
|
||||
{
|
||||
if (!file.TryRelativeTo(rootIter, out var relative))
|
||||
var relPath = Path.GetRelativePath(rootIter, args.FullPath);
|
||||
if (relPath == args.FullPath)
|
||||
{
|
||||
// Not relative.
|
||||
continue;
|
||||
}
|
||||
|
||||
_reloadQueue.Add(relative.Value);
|
||||
var file = ResPath.FromRelativeSystemPath(relPath);
|
||||
_reloadQueue.Add(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,8 +24,9 @@ namespace Robust.Packaging;
|
||||
/// PresetPassesCore --2--> Output
|
||||
/// InputResources --> PresetPassesResources
|
||||
/// PresetPassesResources --1--> AudioMetadata
|
||||
/// PresetPassesResources --2--> NormalizeTextResources
|
||||
/// PresetPassesResources --3--> PrefixResources
|
||||
/// PresetPassesResources --2--> DropAudioFiles
|
||||
/// PresetPassesResources --3--> NormalizeTextResources
|
||||
/// PresetPassesResources --4--> PrefixResources
|
||||
/// AudioMetadata --> PrefixResources
|
||||
/// NormalizeTextResources --> PrefixResources
|
||||
/// PrefixResources --> Output
|
||||
@@ -59,6 +60,17 @@ public sealed class RobustServerAssetGraph
|
||||
public AssetPassPipe PresetPassesResources { get; }
|
||||
public AssetPassAudioMetadata AudioMetadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to drop all files in the <c>Audio/</c> directory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Most audio files are actually removed by <see cref="AudioMetadata"/>.
|
||||
/// This pass cleans up stuff like attribution files and soundfonts in <c>Audio/MidiCustom</c>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public AssetPassFilterDrop DropAudioFiles { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes text files in resources.
|
||||
/// </summary>
|
||||
@@ -103,10 +115,12 @@ public sealed class RobustServerAssetGraph
|
||||
PresetPassesResources = new AssetPassPipe { Name = "RobustServerAssetGraphPresetPassesResources" };
|
||||
NormalizeTextResources = new AssetPassNormalizeText { Name = "RobustServerAssetGraphNormalizeTextResources" };
|
||||
AudioMetadata = new AssetPassAudioMetadata { Name = "RobustServerAssetGraphAudioMetadata" };
|
||||
DropAudioFiles = new AssetPassFilterDrop(p => p.Path.StartsWith("Audio/")) { Name = "RobustServerAssetGraphDropAudioFiles" };
|
||||
PrefixResources = new AssetPassPrefix("Resources/") { Name = "RobustServerAssetGraphPrefixResources" };
|
||||
|
||||
PresetPassesResources.AddDependency(InputResources);
|
||||
AudioMetadata.AddDependency(PresetPassesResources).AddBefore(NormalizeTextResources);
|
||||
AudioMetadata.AddDependency(PresetPassesResources).AddBefore(DropAudioFiles);
|
||||
DropAudioFiles.AddDependency(PresetPassesResources).AddBefore(NormalizeTextResources);
|
||||
NormalizeTextResources.AddDependency(PresetPassesResources).AddBefore(PrefixResources);
|
||||
PrefixResources.AddDependency(PresetPassesResources);
|
||||
PrefixResources.AddDependency(AudioMetadata);
|
||||
@@ -124,6 +138,7 @@ public sealed class RobustServerAssetGraph
|
||||
NormalizeTextResources,
|
||||
AudioMetadata,
|
||||
PrefixResources,
|
||||
DropAudioFiles
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ public sealed class RobustServerPackaging
|
||||
{
|
||||
"Textures",
|
||||
"Fonts",
|
||||
"EngineFonts",
|
||||
"Shaders",
|
||||
"Midi"
|
||||
};
|
||||
|
||||
public static async Task WriteServerResources(
|
||||
|
||||
@@ -102,8 +102,8 @@ namespace Robust.Server.Console.Commands
|
||||
var sys = _system.GetEntitySystem<SharedMapSystem>();
|
||||
if (!sys.MapExists(mapId))
|
||||
{
|
||||
shell.WriteError("Target map does not exist.");
|
||||
return;
|
||||
shell.WriteError($"MapID {intMapId} did not exist, creating without map init");
|
||||
sys.CreateMap(mapId, false); // doesnt runmapinit to be conservative.
|
||||
}
|
||||
|
||||
Vector2 offset = default;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Linq;
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Shared;
|
||||
@@ -18,13 +19,15 @@ namespace Robust.Server.GameObjects
|
||||
|
||||
private bool _deleteEmptyGrids;
|
||||
|
||||
protected override MapId GetNextMapId()
|
||||
[Pure]
|
||||
internal override MapId GetNextMapId()
|
||||
{
|
||||
var id = new MapId(++LastMapId);
|
||||
var id = new MapId(LastMapId + 1);
|
||||
while (MapExists(id) || UsedIds.Contains(id))
|
||||
{
|
||||
id = new MapId(++LastMapId);
|
||||
id = new MapId(id.Value + 1);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ namespace Robust.Server.ServerStatus
|
||||
{
|
||||
context.ResponseHeaders.Add("Content-Encoding", "zstd");
|
||||
|
||||
await context.RespondAsync(result.ManifestData, HttpStatusCode.OK);
|
||||
await context.RespondAsync(result.ManifestData, HttpStatusCode.OK, "text/plain; charset=utf-8");
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -110,7 +110,7 @@ namespace Robust.Server.ServerStatus
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.RespondAsync(result.ManifestData, HttpStatusCode.OK);
|
||||
await context.RespondAsync(result.ManifestData, HttpStatusCode.OK, "text/plain; charset=utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@ public static class Matrix3Helpers
|
||||
return a.EqualsApprox(b, (float) tolerance);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Obsolete("Use TransformBox")]
|
||||
// This method was previously broken, and now just returns an bounding box pretending to be a Box2Rotated
|
||||
public static Box2Rotated TransformBounds(this Matrix3x2 refFromBox, Box2Rotated box)
|
||||
{
|
||||
var matty = Matrix3x2.Multiply(refFromBox, box.Transform);
|
||||
return new Box2Rotated(Vector2.Transform(box.BottomLeft, matty), Vector2.Transform(box.TopRight, matty));
|
||||
return new Box2Rotated(TransformBox(refFromBox, box));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
|
||||
@@ -6,4 +6,3 @@
|
||||
|
||||
[assembly: InternalsVisibleTo("Robust.Client")]
|
||||
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
|
||||
[assembly: InternalsVisibleTo("Content.Benchmarks")]
|
||||
|
||||
@@ -437,7 +437,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
|
||||
protected TimeSpan GetAudioLength(string filename)
|
||||
{
|
||||
if (!filename.StartsWith('/'))
|
||||
throw new ArgumentException("Path must be rooted");
|
||||
throw new ArgumentException($"Path must be rooted. Path: {filename}");
|
||||
return GetAudioLengthImpl(filename);
|
||||
}
|
||||
|
||||
|
||||
@@ -132,13 +132,13 @@ namespace Robust.Shared
|
||||
/// Whether to interpolate between server game states for render frames on the client.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> NetInterp =
|
||||
CVarDef.Create("net.interp", true, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
CVarDef.Create("net.interp", true, CVar.ARCHIVE | CVar.CLIENT | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// The target number of game states to keep buffered up to smooth out network inconsistency.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> NetBufferSize =
|
||||
CVarDef.Create("net.buffer_size", 2, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
CVarDef.Create("net.buffer_size", 2, CVar.ARCHIVE | CVar.CLIENT | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum size of the game state buffer. If this is exceeded the client will request a full game state.
|
||||
@@ -1002,6 +1002,15 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<bool> DisplayVSync =
|
||||
CVarDef.Create("display.vsync", true, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Maximum framerate the client should run at. Set to 0 to have no limit.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is ignored if <see cref="DisplayVSync"/> is enabled.
|
||||
/// </remarks>
|
||||
public static readonly CVarDef<int> DisplayMaxFPS =
|
||||
CVarDef.Create("display.max_fps", 0, CVar.ARCHIVE | CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Window mode for the main game window. 0 = windowed, 1 = fullscreen.
|
||||
/// </summary>
|
||||
|
||||
@@ -33,9 +33,23 @@ sealed class AddMapCommand : LocalizedEntityCommands
|
||||
|
||||
shell.WriteError($"Map with ID {mapId} already exists!");
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
switch (args.Length)
|
||||
{
|
||||
case 1:
|
||||
var mapId = _mapSystem.GetNextMapId();
|
||||
return CompletionResult.FromHintOptions([ new CompletionOption($"{mapId}")], LocalizationManager.GetString("generic-mapid"));
|
||||
case 2:
|
||||
return CompletionResult.FromHint(LocalizationManager.GetString("cmd-addmap-hint-2"));
|
||||
default:
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class RemoveMapCommand : LocalizedCommands
|
||||
sealed class RemoveMapCommand : LocalizedEntityCommands
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _systems = default!;
|
||||
|
||||
@@ -62,6 +76,14 @@ sealed class RemoveMapCommand : LocalizedCommands
|
||||
mapSystem.DeleteMap(mapId);
|
||||
shell.WriteLine($"Map {mapId.Value} was removed.");
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
return CompletionResult.Empty;
|
||||
|
||||
return CompletionResult.FromHintOptions(CompletionHelper.MapIds(args[0], entManager: EntityManager), LocalizationManager.GetString("generic-map"));
|
||||
}
|
||||
}
|
||||
|
||||
sealed class RemoveGridCommand : LocalizedEntityCommands
|
||||
@@ -88,6 +110,14 @@ sealed class RemoveGridCommand : LocalizedEntityCommands
|
||||
EntityManager.DeleteEntity(gridId);
|
||||
shell.WriteLine($"Grid {gridId} was removed.");
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
return CompletionResult.Empty;
|
||||
|
||||
return CompletionResult.FromHintOptions(CompletionHelper.Components<MapGridComponent>(args[0], entManager: EntityManager), LocalizationManager.GetString("generic-grid"));
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RunMapInitCommand : LocalizedEntityCommands
|
||||
@@ -153,7 +183,8 @@ internal sealed class ListMapsCommand : LocalizedEntityCommands
|
||||
string.Join(",", _map.GetAllGrids(mapId).Select(grid => grid.Owner)));
|
||||
}
|
||||
|
||||
shell.WriteLine(msg.ToString());
|
||||
// Trim the newline
|
||||
shell.WriteLine(msg.ToString()[..^1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,6 +214,6 @@ internal sealed class ListGridsCommand : LocalizedEntityCommands
|
||||
uid, xform.MapID, uid, worldPos.X, worldPos.Y);
|
||||
}
|
||||
|
||||
shell.WriteLine(msg.ToString());
|
||||
shell.WriteLine(msg.ToString()[..^1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,11 +208,16 @@ public static class CompletionHelper
|
||||
return sorted ? playerOptions.OrderBy(o => o.Value) : playerOptions;
|
||||
}
|
||||
|
||||
public static IEnumerable<CompletionOption> MapIds(string text, IEntityManager entManager)
|
||||
{
|
||||
return GetComponents<MapComponent>(text, entManager: entManager).Select(o => new CompletionOption(o.Component.MapId.ToString(), o.EntityName));
|
||||
}
|
||||
|
||||
public static IEnumerable<CompletionOption> MapIds(IEntityManager? entManager = null)
|
||||
{
|
||||
IoCManager.Resolve(ref entManager);
|
||||
|
||||
return entManager.EntityQuery<MapComponent>(true).Select(o => new CompletionOption(o.MapId.ToString()));
|
||||
return entManager.EntityQuery<MapComponent, MetaDataComponent>(true).Select(o => new CompletionOption(o.Item1.MapId.ToString(), o.Item2.EntityName));
|
||||
}
|
||||
|
||||
public static IEnumerable<CompletionOption> MapUids(IEntityManager? entManager = null)
|
||||
@@ -245,23 +250,29 @@ public static class CompletionHelper
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<CompletionOption> Components<T>(string text, IEntityManager? entManager = null, int limit = 20) where T : IComponent
|
||||
private static IEnumerable<(T Component, string NetString, string EntityName)> GetComponents<T>(string text, IEntityManager entManager, int limit = 20)
|
||||
where T : IComponent
|
||||
{
|
||||
if (text != string.Empty && !NetEntity.TryParse(text, out _))
|
||||
yield break;
|
||||
|
||||
IoCManager.Resolve(ref entManager);
|
||||
var query = entManager.AllEntityQueryEnumerator<T, MetaDataComponent>();
|
||||
|
||||
var i = 0;
|
||||
while (i < limit && query.MoveNext(out _, out var metadata))
|
||||
while (i < limit && query.MoveNext(out var comp, out var metadata))
|
||||
{
|
||||
var netString = metadata.NetEntity.ToString();
|
||||
if (!netString.StartsWith(text))
|
||||
continue;
|
||||
|
||||
i++;
|
||||
yield return new CompletionOption(netString, metadata.EntityName);
|
||||
yield return (comp, netString, metadata.EntityName);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<CompletionOption> Components<T>(string text, IEntityManager? entManager = null, int limit = 20) where T : IComponent
|
||||
{
|
||||
IoCManager.Resolve(ref entManager);
|
||||
return GetComponents<T>(text, entManager, limit).Select(o => new CompletionOption(o.NetString, o.EntityName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,7 +631,7 @@ namespace Robust.Shared.Containers
|
||||
continue;
|
||||
|
||||
Remove(ent, container, force: true);
|
||||
Del(ent);
|
||||
PredictedDel(ent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ namespace Robust.Shared.ContentPack
|
||||
public string SystemAssemblyName = default!;
|
||||
public HashSet<VerifierError> AllowedVerifierErrors = default!;
|
||||
public List<string> WhitelistedNamespaces = default!;
|
||||
public List<string> AllowedAssemblyPrefixes = default!;
|
||||
public Dictionary<string, Dictionary<string, TypeConfig>> Types = default!;
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,16 @@ namespace Robust.Shared.ContentPack
|
||||
return false;
|
||||
}
|
||||
|
||||
#pragma warning disable RA0004
|
||||
var loadedConfig = _config.Result;
|
||||
#pragma warning restore RA0004
|
||||
|
||||
if (!loadedConfig.AllowedAssemblyPrefixes.Any(allowedNamePrefix => asmName.StartsWith(allowedNamePrefix)))
|
||||
{
|
||||
_sawmill.Error($"Assembly name '{asmName}' is not allowed for a content assembly");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (VerifyIL)
|
||||
{
|
||||
if (!DoVerifyIL(asmName, resolver, peReader, reader))
|
||||
@@ -179,10 +189,6 @@ namespace Robust.Shared.ContentPack
|
||||
return true;
|
||||
}
|
||||
|
||||
#pragma warning disable RA0004
|
||||
var loadedConfig = _config.Result;
|
||||
#pragma warning restore RA0004
|
||||
|
||||
var badRefs = new ConcurrentBag<EntityHandle>();
|
||||
|
||||
// We still do explicit type reference scanning, even though the actual whitelists work with raw members.
|
||||
|
||||
@@ -139,6 +139,7 @@ namespace Robust.Shared.ContentPack
|
||||
/// Returns a list of paths to all top-level content directories
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[Obsolete("This API is no longer content-accessible")]
|
||||
IEnumerable<ResPath> GetContentRoots();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -69,5 +70,7 @@ namespace Robust.Shared.ContentPack
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
bool TryGetDiskFilePath(ResPath path, [NotNullWhen(true)] out string? diskPath);
|
||||
|
||||
new IEnumerable<string> GetContentRoots();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,19 +93,23 @@ namespace Robust.Shared.ContentPack
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
Sawmill.Debug("LOADING modules");
|
||||
var files = new Dictionary<string, (ResPath Path, string[] references)>();
|
||||
var files = new Dictionary<string, (ResPath Path, MemoryStream data, string[] references)>();
|
||||
|
||||
// Find all modules we want to load.
|
||||
foreach (var fullPath in paths)
|
||||
{
|
||||
using var asmFile = _res.ContentFileRead(fullPath);
|
||||
var refData = GetAssemblyReferenceData(asmFile);
|
||||
var ms = new MemoryStream();
|
||||
asmFile.CopyTo(ms);
|
||||
|
||||
ms.Position = 0;
|
||||
var refData = GetAssemblyReferenceData(ms);
|
||||
if (refData == null)
|
||||
continue;
|
||||
|
||||
var (asmRefs, asmName) = refData.Value;
|
||||
|
||||
if (!files.TryAdd(asmName, (fullPath, asmRefs)))
|
||||
if (!files.TryAdd(asmName, (fullPath, ms, asmRefs)))
|
||||
{
|
||||
Sawmill.Error("Found multiple modules with the same assembly name " +
|
||||
$"'{asmName}', A: {files[asmName].Path}, B: {fullPath}.");
|
||||
@@ -122,10 +126,10 @@ namespace Robust.Shared.ContentPack
|
||||
|
||||
Parallel.ForEach(files, pair =>
|
||||
{
|
||||
var (name, (path, _)) = pair;
|
||||
var (name, (_, data, _)) = pair;
|
||||
|
||||
using var stream = _res.ContentFileRead(path);
|
||||
if (!typeChecker.CheckAssembly(stream, resolver))
|
||||
data.Position = 0;
|
||||
if (!typeChecker.CheckAssembly(data, resolver))
|
||||
{
|
||||
throw new TypeCheckFailedException($"Assembly {name} failed type checks.");
|
||||
}
|
||||
@@ -137,14 +141,15 @@ namespace Robust.Shared.ContentPack
|
||||
var nodes = TopologicalSort.FromBeforeAfter(
|
||||
files,
|
||||
kv => kv.Key,
|
||||
kv => kv.Value.Path,
|
||||
kv => kv.Value,
|
||||
_ => Array.Empty<string>(),
|
||||
kv => kv.Value.references,
|
||||
allowMissing: true); // missing refs would be non-content assemblies so allow that.
|
||||
|
||||
// Actually load them in the order they depend on each other.
|
||||
foreach (var path in TopologicalSort.Sort(nodes))
|
||||
foreach (var item in TopologicalSort.Sort(nodes))
|
||||
{
|
||||
var (path, memory, _) = item;
|
||||
Sawmill.Debug($"Loading module: '{path}'");
|
||||
try
|
||||
{
|
||||
@@ -156,9 +161,9 @@ namespace Robust.Shared.ContentPack
|
||||
}
|
||||
else
|
||||
{
|
||||
using var assemblyStream = _res.ContentFileRead(path);
|
||||
memory.Position = 0;
|
||||
using var symbolsStream = _res.ContentFileReadOrNull(path.WithExtension("pdb"));
|
||||
LoadGameAssembly(assemblyStream, symbolsStream, skipVerify: true);
|
||||
LoadGameAssembly(memory, symbolsStream, skipVerify: true);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -174,7 +179,7 @@ namespace Robust.Shared.ContentPack
|
||||
|
||||
private (string[] refs, string name)? GetAssemblyReferenceData(Stream stream)
|
||||
{
|
||||
using var reader = ModLoader.MakePEReader(stream);
|
||||
using var reader = ModLoader.MakePEReader(stream, leaveOpen: true);
|
||||
var metaReader = reader.GetMetadataReader();
|
||||
|
||||
var name = metaReader.GetString(metaReader.GetAssemblyDefinition().Name);
|
||||
|
||||
@@ -371,20 +371,19 @@ namespace Robust.Shared.ContentPack
|
||||
AddRoot(ResPath.Root, loader);
|
||||
}
|
||||
|
||||
public IEnumerable<ResPath> GetContentRoots()
|
||||
IEnumerable<ResPath> IResourceManager.GetContentRoots()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetContentRoots()
|
||||
{
|
||||
foreach (var (_, root) in _contentRoots)
|
||||
{
|
||||
if (root is DirLoader loader)
|
||||
{
|
||||
var rootDir = loader.GetPath(new ResPath(@"/"));
|
||||
if (root is not DirLoader loader)
|
||||
continue;
|
||||
|
||||
// TODO: GET RID OF THIS.
|
||||
// This code shouldn't be passing OS disk paths through ResPath.
|
||||
rootDir = rootDir.Replace(Path.DirectorySeparatorChar, '/');
|
||||
|
||||
yield return new ResPath(rootDir);
|
||||
}
|
||||
yield return loader.GetPath(ResPath.Root);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@ WhitelistedNamespaces:
|
||||
- Content
|
||||
- OpenDreamShared
|
||||
|
||||
AllowedAssemblyPrefixes:
|
||||
- OpenDream
|
||||
- Content
|
||||
|
||||
# The type whitelist does NOT care about which assembly types come from.
|
||||
# This is because types switch assembly all the time.
|
||||
# Just look up stuff like StreamReader on https://apisof.net.
|
||||
@@ -529,6 +533,7 @@ Types:
|
||||
- "string ToTitleCase(string)"
|
||||
- "string ToTitleCase(string)"
|
||||
- "string ToUpper(string)"
|
||||
UnicodeCategory: { } # Enum
|
||||
System.IO.Compression:
|
||||
CompressionMode: { } # Enum
|
||||
CompressionLevel: { } # Enum
|
||||
@@ -1559,6 +1564,11 @@ Types:
|
||||
ValueType: { All: True }
|
||||
Version: { All: True }
|
||||
Void: { All: True }
|
||||
WeakReference`1:
|
||||
Methods:
|
||||
- "void .ctor(!0)"
|
||||
- "bool TryGetTarget(ref !0)"
|
||||
- "void SetTarget(!0)"
|
||||
YamlDotNet.Core.Events:
|
||||
MappingStyle: { } # Enum
|
||||
SequenceStyle: { } # Enum
|
||||
@@ -1608,3 +1618,7 @@ Types:
|
||||
SourcePos: { }
|
||||
Pidgin.Configuration:
|
||||
IConfiguration`1: { All: True }
|
||||
SpaceWizards.Sodium:
|
||||
CryptoGenericHashBlake2B:
|
||||
Methods:
|
||||
- "byte[] Hash(int, System.ReadOnlySpan`1<byte>, System.ReadOnlySpan`1<byte>)"
|
||||
|
||||
@@ -685,38 +685,38 @@ public sealed class EntityDeserializer :
|
||||
|
||||
foreach (var yamlId in MapYamlIds)
|
||||
{
|
||||
var uid = UidMap[yamlId];
|
||||
if (_mapQuery.TryComp(uid, out var map))
|
||||
if (UidMap.TryGetValue(yamlId, out var uid) && _mapQuery.TryComp(uid, out var map))
|
||||
{
|
||||
Result.Maps.Add((uid, map));
|
||||
EntMan.EnsureComponent<LoadedMapComponent>(uid);
|
||||
}
|
||||
else
|
||||
_log.Error($"Missing map entity: {EntMan.ToPrettyString(uid)}");
|
||||
_log.Error($"Missing map entity: {EntMan.ToPrettyString(uid)}. YamlId: {yamlId}");
|
||||
}
|
||||
|
||||
foreach (var yamlId in GridYamlIds)
|
||||
{
|
||||
var uid = UidMap[yamlId];
|
||||
if (_gridQuery.TryComp(uid, out var grid))
|
||||
if (UidMap.TryGetValue(yamlId, out var uid) && _gridQuery.TryComp(uid, out var grid))
|
||||
Result.Grids.Add((uid, grid));
|
||||
else
|
||||
_log.Error($"Missing grid entity: {EntMan.ToPrettyString(uid)}");
|
||||
_log.Error($"Missing grid entity: {EntMan.ToPrettyString(uid)}. YamlId: {yamlId}");
|
||||
}
|
||||
|
||||
foreach (var yamlId in OrphanYamlIds)
|
||||
{
|
||||
var uid = UidMap[yamlId];
|
||||
if (_mapQuery.HasComponent(uid) || _xformQuery.Comp(uid).ParentUid.IsValid())
|
||||
_log.Error($"Entity {EntMan.ToPrettyString(uid)} was incorrectly labelled as an orphan?");
|
||||
if (!UidMap.TryGetValue(yamlId, out var uid))
|
||||
_log.Error($"Missing orphan entity with YamlId: {yamlId}");
|
||||
else if (_mapQuery.HasComponent(uid) || _xformQuery.Comp(uid).ParentUid.IsValid())
|
||||
_log.Error($"Entity {EntMan.ToPrettyString(uid)} was incorrectly labelled as an orphan? YamlId: {yamlId}");
|
||||
else
|
||||
Result.Orphans.Add(uid);
|
||||
}
|
||||
|
||||
foreach (var yamlId in NullspaceYamlIds)
|
||||
{
|
||||
var uid = UidMap[yamlId];
|
||||
if (_mapQuery.HasComponent(uid) || _xformQuery.Comp(uid).ParentUid.IsValid())
|
||||
if (!UidMap.TryGetValue(yamlId, out var uid))
|
||||
_log.Error($"Missing nullspace entity with YamlId: {yamlId}");
|
||||
else if (_mapQuery.HasComponent(uid) || _xformQuery.Comp(uid).ParentUid.IsValid())
|
||||
_log.Error($"Entity {EntMan.ToPrettyString(uid)} was incorrectly labelled as a null-space entity?");
|
||||
else
|
||||
Result.NullspaceEntities.Add(uid);
|
||||
@@ -1152,6 +1152,7 @@ public sealed class EntityDeserializer :
|
||||
ISerializationContext? context,
|
||||
ISerializationManager.InstantiationDelegate<EntityUid>? _)
|
||||
{
|
||||
string msg;
|
||||
if (node.Value == "invalid")
|
||||
{
|
||||
if (CurrentComponent == "Transform")
|
||||
@@ -1160,7 +1161,7 @@ public sealed class EntityDeserializer :
|
||||
if (!Options.LogInvalidEntities)
|
||||
return EntityUid.Invalid;
|
||||
|
||||
var msg = CurrentReadingEntity is not { } curr
|
||||
msg = CurrentReadingEntity is not { } curr
|
||||
? $"Encountered invalid EntityUid reference"
|
||||
: $"Encountered invalid EntityUid reference wile reading entity {curr.YamlId}, component: {CurrentComponent}";
|
||||
_log.Error(msg);
|
||||
@@ -1170,7 +1171,10 @@ public sealed class EntityDeserializer :
|
||||
if (int.TryParse(node.Value, out var val) && UidMap.TryGetValue(val, out var entity))
|
||||
return entity;
|
||||
|
||||
_log.Error($"Invalid yaml entity id: '{val}'");
|
||||
msg = CurrentReadingEntity is not { } ent
|
||||
? "Encountered unknown entity yaml uid"
|
||||
: $"Encountered unknown entity yaml uid wile reading entity {ent.YamlId}, component: {CurrentComponent}";
|
||||
_log.Error(msg);
|
||||
return EntityUid.Invalid;
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,11 @@ public sealed class EntitySerializer : ISerializationContext,
|
||||
/// </summary>
|
||||
public readonly Dictionary<string, List<int>> Prototypes = new();
|
||||
|
||||
/// <summary>
|
||||
/// Set of entities that have encountered issues during serialization and are now being ignored.
|
||||
/// </summary>
|
||||
public HashSet<EntityUid> ErroringEntities = new();
|
||||
|
||||
/// <summary>
|
||||
/// Yaml ids of all serialized map entities.
|
||||
/// </summary>
|
||||
@@ -412,7 +417,7 @@ public sealed class EntitySerializer : ISerializationContext,
|
||||
|
||||
// It might be possible that something could cause an entity to be included twice.
|
||||
// E.g., if someone serializes a grid w/o its map, and then tries to separately include the map and all its children.
|
||||
// In that case, the grid would already have been serialized as a orphan.
|
||||
// In that case, the grid would already have been serialized as an orphan.
|
||||
// uhhh.... I guess its fine?
|
||||
if (EntityData.ContainsKey(saveId))
|
||||
return;
|
||||
@@ -489,6 +494,95 @@ public sealed class EntitySerializer : ISerializationContext,
|
||||
xform._localRotation = 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SerializeComponents(uid, cache, components);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
if (Options.EntityExceptionBehaviour == EntityExceptionBehaviour.Rethrow)
|
||||
{
|
||||
_log.Error($"Caught exception while serializing component {CurrentComponent} of entity {EntMan.ToPrettyString(uid)}");
|
||||
throw;
|
||||
}
|
||||
|
||||
_log.Error($"Caught exception while serializing component {CurrentComponent} of entity {EntMan.ToPrettyString(uid)}:\n{e}");
|
||||
CurrentEntityYamlUid = 0;
|
||||
CurrentEntity = null;
|
||||
CurrentComponent = null;
|
||||
RemoveErroringEntity(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentComponent = null;
|
||||
if (components.Count != 0)
|
||||
entData.Add("components", components);
|
||||
|
||||
// TODO ENTITY SERIALIZATION
|
||||
// Consider adding a Action<EntityUid, MappingDataNode>? OnEntitySerialized
|
||||
// I.e., allow content to modify the per-entity data? I don't know if that would actually be useful, as content
|
||||
// could just as easily append a separate entity dictionary to the output that has the extra per-entity data they
|
||||
// want to serialize.
|
||||
|
||||
if (meta.EntityPrototype == null)
|
||||
{
|
||||
CurrentEntityYamlUid = 0;
|
||||
CurrentEntity = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// an entity may have fewer components than the original prototype, so we need to check if any are missing.
|
||||
SequenceDataNode? missingComponents = null;
|
||||
foreach (var (name, comp) in meta.EntityPrototype.Components)
|
||||
{
|
||||
// try comp instead of has-comp as it checks whether the component is supposed to have been
|
||||
// deleted.
|
||||
if (EntMan.TryGetComponent(uid, comp.Component.GetType(), out _))
|
||||
continue;
|
||||
|
||||
missingComponents ??= new();
|
||||
missingComponents.Add(new ValueDataNode(name));
|
||||
}
|
||||
|
||||
if (missingComponents != null)
|
||||
entData.Add("missingComponents", missingComponents);
|
||||
|
||||
CurrentEntityYamlUid = 0;
|
||||
CurrentEntity = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove an exception throwing entity (and possibly its children) from the serialized data.
|
||||
/// </summary>
|
||||
private void RemoveErroringEntity(EntityUid uid)
|
||||
{
|
||||
if (Options.EntityExceptionBehaviour == EntityExceptionBehaviour.IgnoreEntityAndChildren)
|
||||
{
|
||||
foreach (var child in _xformQuery.GetComponent(uid)._children)
|
||||
{
|
||||
RemoveErroringEntity(child);
|
||||
}
|
||||
}
|
||||
|
||||
ErroringEntities.Add(uid);
|
||||
if (!YamlUidMap.TryGetValue(uid, out var yamlId))
|
||||
return;
|
||||
|
||||
Nullspace.Remove(yamlId);
|
||||
Orphans.Remove(yamlId);
|
||||
Maps.Remove(yamlId);
|
||||
Grids.Remove(yamlId);
|
||||
EntityData.Remove(yamlId);
|
||||
if (_metaQuery.TryGetComponent(uid, out var meta)
|
||||
&& meta.EntityPrototype != null
|
||||
&& Prototypes.TryGetValue(meta.EntityPrototype.ID, out var proto))
|
||||
{
|
||||
proto.Remove(yamlId);
|
||||
}
|
||||
}
|
||||
|
||||
private void SerializeComponents(EntityUid uid, Dictionary<string, MappingDataNode>? cache, SequenceDataNode components)
|
||||
{
|
||||
foreach (var component in EntMan.GetComponentsInternal(uid))
|
||||
{
|
||||
var compType = component.GetType();
|
||||
@@ -523,48 +617,12 @@ public sealed class EntitySerializer : ISerializationContext,
|
||||
// Don't need to write it if nothing was written! Note that if this entity has no associated
|
||||
// prototype, we ALWAYS want to write the component, because merely the fact that it exists is
|
||||
// information that needs to be written.
|
||||
if (compMapping.Children.Count != 0 || protoMapping == null)
|
||||
{
|
||||
compMapping.InsertAt(0, "type", new ValueDataNode(reg.Name));
|
||||
components.Add(compMapping);
|
||||
}
|
||||
}
|
||||
|
||||
CurrentComponent = null;
|
||||
if (components.Count != 0)
|
||||
entData.Add("components", components);
|
||||
|
||||
// TODO ENTITY SERIALIZATION
|
||||
// Consider adding a Action<EntityUid, MappingDataNode>? OnEntitySerialized
|
||||
// I.e., allow content to modify the per-entity data? I don't know if that would actually be useful, as content
|
||||
// could just as easily append a separate entity dictionary to the output that has the extra per-entity data they
|
||||
// want to serialize.
|
||||
|
||||
if (meta.EntityPrototype == null)
|
||||
{
|
||||
CurrentEntityYamlUid = 0;
|
||||
CurrentEntity = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// an entity may have less components than the original prototype, so we need to check if any are missing.
|
||||
SequenceDataNode? missingComponents = null;
|
||||
foreach (var (name, comp) in meta.EntityPrototype.Components)
|
||||
{
|
||||
// try comp instead of has-comp as it checks whether the component is supposed to have been
|
||||
// deleted.
|
||||
if (EntMan.TryGetComponent(uid, comp.Component.GetType(), out _))
|
||||
if (compMapping.Children.Count == 0 && protoMapping != null)
|
||||
continue;
|
||||
|
||||
missingComponents ??= new();
|
||||
missingComponents.Add(new ValueDataNode(name));
|
||||
compMapping.InsertAt(0, "type", new ValueDataNode(reg.Name));
|
||||
components.Add(compMapping);
|
||||
}
|
||||
|
||||
if (missingComponents != null)
|
||||
entData.Add("missingComponents", missingComponents);
|
||||
|
||||
CurrentEntityYamlUid = 0;
|
||||
CurrentEntity = null;
|
||||
}
|
||||
|
||||
private Dictionary<string, MappingDataNode>? GetProtoCache(EntityPrototype? proto)
|
||||
@@ -656,7 +714,10 @@ public sealed class EntitySerializer : ISerializationContext,
|
||||
|
||||
public SequenceDataNode WriteEntitySection()
|
||||
{
|
||||
if (YamlIds.Count != YamlUidMap.Count || YamlIds.Count != EntityData.Count)
|
||||
// Check that EntityData contains the expected number of entities.
|
||||
if (Options.EntityExceptionBehaviour != EntityExceptionBehaviour.IgnoreEntity
|
||||
&& Options.EntityExceptionBehaviour != EntityExceptionBehaviour.IgnoreEntityAndChildren
|
||||
&& (YamlIds.Count != YamlUidMap.Count || YamlIds.Count != EntityData.Count))
|
||||
{
|
||||
// Maybe someone reserved a yaml id with ReserveYamlId() or implicitly with GetId() without actually
|
||||
// ever serializing the entity, This can lead to references to non-existent entities.
|
||||
@@ -878,6 +939,7 @@ public sealed class EntitySerializer : ISerializationContext,
|
||||
if (YamlUidMap.TryGetValue(value, out var yamlId))
|
||||
return new ValueDataNode(yamlId.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
|
||||
if (CurrentComponent == _xformName)
|
||||
{
|
||||
if (value == EntityUid.Invalid)
|
||||
@@ -886,12 +948,18 @@ public sealed class EntitySerializer : ISerializationContext,
|
||||
DebugTools.Assert(!Orphans.Contains(CurrentEntityYamlUid));
|
||||
Orphans.Add(CurrentEntityYamlUid);
|
||||
|
||||
if (Options.ErrorOnOrphan && CurrentEntity != null && value != Truncate)
|
||||
if (Options.ErrorOnOrphan && CurrentEntity != null && value != Truncate && !ErroringEntities.Contains(value))
|
||||
_log.Error($"Serializing entity {EntMan.ToPrettyString(CurrentEntity)} without including its parent {EntMan.ToPrettyString(value)}");
|
||||
|
||||
return new ValueDataNode("invalid");
|
||||
}
|
||||
|
||||
if (ErroringEntities.Contains(value))
|
||||
{
|
||||
// Referenced entity already logged an error, so we just silently fail.
|
||||
return new ValueDataNode("invalid");
|
||||
}
|
||||
|
||||
if (value == EntityUid.Invalid)
|
||||
{
|
||||
if (Options.MissingEntityBehaviour != MissingEntityBehaviour.Ignore)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Numerics;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.EntitySerialization.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Log;
|
||||
@@ -21,7 +20,13 @@ public record struct SerializationOptions
|
||||
public MissingEntityBehaviour MissingEntityBehaviour = MissingEntityBehaviour.IncludeNullspace;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not to log an error when serializing an entity without its parent.
|
||||
/// What to do when an exception is thrown while trying to serialize an entity. The default behaviour is to abort
|
||||
/// the serialization.
|
||||
/// </summary>
|
||||
public EntityExceptionBehaviour EntityExceptionBehaviour = EntityExceptionBehaviour.Rethrow;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to log an error when serializing an entity without its parent.
|
||||
/// </summary>
|
||||
public bool ErrorOnOrphan = true;
|
||||
|
||||
|
||||
@@ -86,3 +86,33 @@ public enum MissingEntityBehaviour
|
||||
/// </summary>
|
||||
AutoInclude,
|
||||
}
|
||||
|
||||
|
||||
public enum EntityExceptionBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Re-throw the exception, interrupting the serialization.
|
||||
/// </summary>
|
||||
Rethrow,
|
||||
|
||||
/// <summary>
|
||||
/// Continue serializing and simply skip/ignore this entity. May result in broken maps that log errors or simply
|
||||
/// fail to load.
|
||||
/// </summary>
|
||||
IgnoreEntity,
|
||||
|
||||
/// <summary>
|
||||
/// Continue serializing and simply skip/ignore this entity and all of its children.
|
||||
/// May result in broken maps that log errors or simply fail to load.
|
||||
/// </summary>
|
||||
IgnoreEntityAndChildren,
|
||||
|
||||
// TODO SERIALIZATION
|
||||
/*
|
||||
/// <summary>
|
||||
/// Continue the serialization while skipping over the component that caused the exception to be thrown. May result
|
||||
/// in broken maps that log errors or simply fail to load.
|
||||
/// </summary>
|
||||
IgnoreComponent,
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ public sealed partial class MapLoaderSystem
|
||||
|
||||
// Using a local deserializer instead of a cached value, both to ensure that we don't accidentally carry over
|
||||
// data from a previous serializations, and because some entities cause other maps/grids to be loaded during
|
||||
// during mapinit.
|
||||
// mapinit.
|
||||
var deserializer = new EntityDeserializer(
|
||||
_dependency,
|
||||
data,
|
||||
@@ -124,6 +124,17 @@ public sealed partial class MapLoaderSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the file isn't of the expected category, stop before we ever create any entities.
|
||||
if (opts.ExpectedCategory is { } expected
|
||||
&& expected != deserializer.Result.Category
|
||||
&& deserializer.Result.Category != FileCategory.Unknown)
|
||||
{
|
||||
// Did someone try to load a map file as a grid or vice versa?
|
||||
Log.Error($"Map {fileName} does not contain the expected data. Expected {expected} but got {deserializer.Result.Category}");
|
||||
Delete(deserializer.Result);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
deserializer.CreateEntities();
|
||||
@@ -135,6 +146,8 @@ public sealed partial class MapLoaderSystem
|
||||
throw;
|
||||
}
|
||||
|
||||
// If the map file was an older version, the category has to be inferred from the file's contents in CreateEntities()
|
||||
// Hence the category is checked again here.
|
||||
if (opts.ExpectedCategory is { } exp && exp != deserializer.Result.Category)
|
||||
{
|
||||
// Did someone try to load a map file as a grid or vice versa?
|
||||
|
||||
@@ -14,6 +14,10 @@ public sealed class PointLightComponentState : ComponentState
|
||||
|
||||
public float Softness;
|
||||
|
||||
public float Falloff;
|
||||
|
||||
public float CurveFactor;
|
||||
|
||||
public bool CastShadows;
|
||||
|
||||
public bool Enabled;
|
||||
|
||||
@@ -6,6 +6,7 @@ using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Robust.Shared.GameObjects
|
||||
{
|
||||
@@ -33,6 +34,39 @@ namespace Robust.Shared.GameObjects
|
||||
[DataField("softness"), Animatable]
|
||||
public float Softness { get; set; } = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Controls how quickly the light falls off in power in its radius.
|
||||
/// A higher value means a stronger falloff.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default value of 6.8 might seem suspect, but that's because this is a value which was introduced
|
||||
/// years after SS14 already standardized its light values using an older attenuation curve, and this was the value
|
||||
/// which, qualitatively, seemed about equivalent in brightness for the large majority of lights on the station
|
||||
/// compared to the old function.
|
||||
///
|
||||
/// See https://www.desmos.com/calculator/yjudaha0s6 for a demonstration of how this value affects the shape of the curve
|
||||
/// for different light radii and curve factors.
|
||||
/// </remarks>
|
||||
[DataField, Animatable]
|
||||
public float Falloff { get; set; } = 6.8f;
|
||||
|
||||
/// <summary>
|
||||
/// Controls the shape of the curve used for point light attenuation.
|
||||
/// This value may vary between 0 and 1.
|
||||
/// A value of 0 gives a shape roughly equivalent to 1/1+distance (more or less realistic),
|
||||
/// while a value of 1 gives a shape roughly equivalent to 1+distance^2 (closer to a sphere-shaped light)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This does not directly control the exponent of the denominator, though it might seem that way.
|
||||
/// Rather, it just lerps between an inverse-shaped curve and an inverse-quadratic-shaped curve.
|
||||
/// Values below 0 or above 1 are nonsensical.
|
||||
///
|
||||
/// See https://www.desmos.com/calculator/yjudaha0s6 for a demonstration of how this value affects the shape of the curve
|
||||
/// for different light radii and falloff values.
|
||||
/// </remarks>
|
||||
[DataField, Animatable]
|
||||
public float CurveFactor { get; set; } = 0.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this pointlight should cast shadows
|
||||
/// </summary>
|
||||
|
||||
@@ -49,6 +49,7 @@ public sealed partial class PrototypeLayerData
|
||||
[DataField] public PrototypeCopyToShaderParameters? CopyToShaderParameters;
|
||||
|
||||
[DataField] public bool Cycle;
|
||||
[DataField] public bool Loop = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user