mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdc8832e4c | ||
|
|
033205f7df | ||
|
|
b2ab349e1c | ||
|
|
6fea24f474 | ||
|
|
dbde8023ed | ||
|
|
83279ff285 | ||
|
|
14439784dd | ||
|
|
80cad0cd8f | ||
|
|
b15d960c69 | ||
|
|
cd59027089 | ||
|
|
015bf3318b | ||
|
|
3f19d25018 | ||
|
|
fe19ff9bd5 | ||
|
|
c95b4320cf | ||
|
|
4755cb5747 | ||
|
|
7bc0ffb711 | ||
|
|
e49515956a | ||
|
|
f3a3f564e1 | ||
|
|
8b7fbfa646 | ||
|
|
bb4c4ed302 | ||
|
|
c9009342b6 | ||
|
|
3a337e4842 | ||
|
|
e788325cdb | ||
|
|
9a0e3b6b02 | ||
|
|
37eabbabc2 | ||
|
|
ab775af7cd | ||
|
|
8ac5fc58d2 | ||
|
|
37c7aa544e | ||
|
|
7542b1ca16 | ||
|
|
8235bd8478 | ||
|
|
1657a49c1c | ||
|
|
669b515ce6 | ||
|
|
8478e62a3e | ||
|
|
034728258c | ||
|
|
b0fec0fd76 | ||
|
|
665294bee8 | ||
|
|
4b04081749 | ||
|
|
d3a9199b8e | ||
|
|
6fb9ff7554 |
@@ -62,6 +62,7 @@
|
||||
<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="SpaceWizards.Fontconfig.Interop" Version="1.0.0" />
|
||||
<PackageVersion Include="libsodium" Version="1.0.20.1" />
|
||||
<PackageVersion Include="System.Management" Version="9.0.8" />
|
||||
<PackageVersion Include="TerraFX.Interop.Windows" Version="10.0.26100.1" />
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</When>
|
||||
<Otherwise>
|
||||
<PropertyGroup>
|
||||
<DefineConstants>$(DefineConstants);LINUX;UNIX</DefineConstants>
|
||||
<DefineConstants>$(DefineConstants);LINUX;UNIX;FREEDESKTOP</DefineConstants>
|
||||
</PropertyGroup>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project>
|
||||
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
<!-- This file automatically reset by Tools/version.py -->
|
||||
|
||||
|
||||
@@ -31,5 +31,6 @@
|
||||
<Python>python3</Python>
|
||||
<Python Condition="'$(ActualOS)' == 'Windows'">py -3</Python>
|
||||
<UseSystemSqlite Condition="'$(TargetOS)' == 'FreeBSD'">True</UseSystemSqlite>
|
||||
<IsFreedesktop Condition="'$(TargetOS)' == 'FreeBSD' Or '$(TargetOS)' == 'Linux'">True</IsFreedesktop>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -54,6 +54,63 @@ END TEMPLATE-->
|
||||
*None yet*
|
||||
|
||||
|
||||
## 268.0.2
|
||||
|
||||
|
||||
## 268.0.1
|
||||
|
||||
|
||||
## 268.0.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Events that are raised via `IEventBus.RaiseComponentEvent()` now **must** be annotated with the `ComponentEventAttribute`.
|
||||
* By default, events annotated with this attribute can **only** be raised via `IEventBus.RaiseComponentEvent()`. This can be configured via `ComponentEventAttribute.Exclusive`
|
||||
* StartCollide and EndCollide events are now buffered until the end of physics substeps instead of being raised during the CollideContacts step. EndCollide events are double-buffered and any new ones raised while the events are being dispatched will now go out on the next tick / substep.
|
||||
|
||||
### New features
|
||||
|
||||
* Added `IUserInterfaceManager.ControlSawmill` and `Control.Log` properties so that controls can easily use logging without using static methods.
|
||||
|
||||
|
||||
## 267.4.0
|
||||
|
||||
### New features
|
||||
|
||||
* Added two new custom yaml serializers `CustomListSerializer` and `CustomArraySerializer`.
|
||||
* CVars defined in `[CVarDefs]` can now be private or internal.
|
||||
* Added config rollback system to `IConfigurationManager`. This enables CVars to be snapshot and rolled back, even in the event of client crash.
|
||||
* `OptionButton` now has a `Filterable` property that gives it a text box to filter options.
|
||||
* Added `FontTagHijackHolder` to replace fonts resolved by `FontTag`.
|
||||
* Sandbox:
|
||||
* Exposed `System.Reflection.Metadata.MetadataUpdateHandlerAttribute`.
|
||||
* Exposed more overloads on `StringBuilder`.
|
||||
* The engine can now load system fonts.
|
||||
* At the moment only available on Windows.
|
||||
* See `ISystemFontManager` for API.
|
||||
* The client now display a loading screen during startup.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fix `Menu` and `NumpadDecimal` key codes on SDL3.
|
||||
* client-side predicted entity deletion ( `EntityManager.PredictedQueueDeleteEntity`) now behaves more like it does on the server. In particular, entities will be deleted on the same tick after all system have been updated. Previously, it would process deletions at the beginning of the next tick.
|
||||
* Fix modifying `Label.FontOverride` not causing a layout update.
|
||||
* Controls created by rich-text tags now get arranged to a proper size.
|
||||
* Fix `OutputPanel` scrollbar breaking if a style update changes the font size.
|
||||
|
||||
### Other
|
||||
|
||||
* ComponentNameSerializer will now ignore any components that have been ignored via `IComponentFactory.RegisterIgnore`.
|
||||
* Add pure to some SharedTransformSystem methods.
|
||||
* Significantly optimised collision detection in SharedBroadphaseSystem.
|
||||
* `Control.Stylesheet` does not do any work if assigning the value it already has.
|
||||
* XAML hot reload now JITs UIs when first opened rather than doing every single one at client startup. This reduces dev startup overhead significantly and probably helps with memory usage too.
|
||||
|
||||
### Internal
|
||||
|
||||
* The `dmetamem` command now sorts its output, and doesn't output to log anymore to avoid output interleaving.
|
||||
|
||||
|
||||
## 267.3.0
|
||||
|
||||
### New features
|
||||
|
||||
@@ -8,3 +8,5 @@ color-selector-sliders-alpha = A
|
||||
|
||||
color-selector-sliders-rgb = RGB
|
||||
color-selector-sliders-hsv = HSV
|
||||
|
||||
option-button-filter = Filter
|
||||
|
||||
19
Robust.Benchmarks/NumericsHelpers/Box2Benchmark.cs
Normal file
19
Robust.Benchmarks/NumericsHelpers/Box2Benchmark.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Numerics;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Benchmarks.NumericsHelpers;
|
||||
|
||||
[Virtual, DisassemblyDiagnoser]
|
||||
public class Box2Benchmark
|
||||
{
|
||||
public Box2 Box = new();
|
||||
public Matrix3x2 Matrix = new();
|
||||
|
||||
[Benchmark]
|
||||
public Box2 Transform()
|
||||
{
|
||||
return Matrix.TransformBox(Box);
|
||||
}
|
||||
}
|
||||
18
Robust.Benchmarks/NumericsHelpers/Box2RotatedBenchmark.cs
Normal file
18
Robust.Benchmarks/NumericsHelpers/Box2RotatedBenchmark.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Numerics;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Benchmarks.NumericsHelpers;
|
||||
|
||||
[Virtual, DisassemblyDiagnoser]
|
||||
public class Box2RotatedBenchmark
|
||||
{
|
||||
public Box2Rotated Box = new();
|
||||
|
||||
[Benchmark]
|
||||
public Matrix3x2 GetTransform()
|
||||
{
|
||||
return Box.Transform;
|
||||
}
|
||||
}
|
||||
25
Robust.Benchmarks/NumericsHelpers/GetAABBBenchmark.cs
Normal file
25
Robust.Benchmarks/NumericsHelpers/GetAABBBenchmark.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Runtime.Intrinsics;
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Robust.Benchmarks.NumericsHelpers;
|
||||
|
||||
[Virtual, DisassemblyDiagnoser]
|
||||
public class GetAABBBenchmark
|
||||
{
|
||||
public Vector128<float> X;
|
||||
public Vector128<float> Y;
|
||||
|
||||
[Benchmark(Baseline = true)]
|
||||
public Vector128<float> GetAABB_NoAvx()
|
||||
{
|
||||
return SimdHelpers.GetAABBSlow(X, Y);
|
||||
}
|
||||
|
||||
[Benchmark]
|
||||
public Vector128<float> GetAABB_Avx()
|
||||
{
|
||||
return SimdHelpers.GetAABBAvx(X, Y);
|
||||
}
|
||||
}
|
||||
21
Robust.Benchmarks/NumericsHelpers/SlimPolygonBenchmark.cs
Normal file
21
Robust.Benchmarks/NumericsHelpers/SlimPolygonBenchmark.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using BenchmarkDotNet.Attributes;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics.Shapes;
|
||||
|
||||
namespace Robust.Benchmarks.NumericsHelpers;
|
||||
|
||||
// SlimPolyon is internal, so this won't compile without changes.
|
||||
/*
|
||||
[Virtual, DisassemblyDiagnoser]
|
||||
public class SlimPolygonBenchmark
|
||||
{
|
||||
public Box2Rotated RotBox = new();
|
||||
|
||||
[Benchmark]
|
||||
public SlimPolygon RotatedBox()
|
||||
{
|
||||
return new SlimPolygon(in RotBox);
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -8,6 +8,7 @@ using Robust.Client.GameObjects;
|
||||
using Robust.Client.GameStates;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Clyde;
|
||||
using Robust.Client.Graphics.FontManagement;
|
||||
using Robust.Client.HWId;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Localization;
|
||||
@@ -67,6 +68,7 @@ namespace Robust.Client
|
||||
deps.Register<IMapManagerInternal, NetworkedMapManager>();
|
||||
deps.Register<INetworkedMapManager, NetworkedMapManager>();
|
||||
deps.Register<IEntityManager, ClientEntityManager>();
|
||||
deps.Register<FontTagHijackHolder>();
|
||||
deps.Register<IReflectionManager, ClientReflectionManager>();
|
||||
deps.Register<IConsoleHost, ClientConsoleHost>();
|
||||
deps.Register<IClientConsoleHost, ClientConsoleHost>();
|
||||
@@ -108,6 +110,8 @@ namespace Robust.Client
|
||||
deps.Register<IReloadManager, ReloadManager>();
|
||||
deps.Register<ILocalizationManager, ClientLocalizationManager>();
|
||||
deps.Register<ILocalizationManagerInternal, ClientLocalizationManager>();
|
||||
deps.Register<LoadingScreenManager>();
|
||||
deps.Register<ILoadingScreenManager, LoadingScreenManager>();
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
@@ -120,6 +124,8 @@ namespace Robust.Client
|
||||
deps.Register<IInputManager, InputManager>();
|
||||
deps.Register<IFileDialogManager, DummyFileDialogManager>();
|
||||
deps.Register<IUriOpener, UriOpenerDummy>();
|
||||
deps.Register<ISystemFontManager, SystemFontManagerFallback>();
|
||||
deps.Register<ISystemFontManagerInternal, SystemFontManagerFallback>();
|
||||
break;
|
||||
case GameController.DisplayMode.Clyde:
|
||||
deps.Register<IClyde, Clyde>();
|
||||
@@ -130,6 +136,8 @@ namespace Robust.Client
|
||||
deps.Register<IInputManager, ClydeInputManager>();
|
||||
deps.Register<IFileDialogManager, FileDialogManager>();
|
||||
deps.Register<IUriOpener, UriOpener>();
|
||||
deps.Register<ISystemFontManager, SystemFontManager>();
|
||||
deps.Register<ISystemFontManagerInternal, SystemFontManager>();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.ContentPack;
|
||||
|
||||
@@ -19,10 +20,28 @@ namespace Robust.Client.Console.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var sig in AssemblyTypeChecker.DumpMetaMembers(type))
|
||||
var members = AssemblyTypeChecker.DumpMetaMembers(type)
|
||||
.GroupBy(x => x.IsField)
|
||||
.ToDictionary(x => x.Key, x => x.Select(t => t.Value).ToList());
|
||||
|
||||
if (members.TryGetValue(true, out var fields))
|
||||
{
|
||||
System.Console.WriteLine(@$"- ""{sig}""");
|
||||
shell.WriteLine(sig);
|
||||
fields.Sort(StringComparer.Ordinal);
|
||||
|
||||
foreach (var member in fields)
|
||||
{
|
||||
System.Console.WriteLine(@$"- ""{member}""");
|
||||
}
|
||||
}
|
||||
|
||||
if (members.TryGetValue(false, out var methods))
|
||||
{
|
||||
methods.Sort(StringComparer.Ordinal);
|
||||
|
||||
foreach (var member in methods)
|
||||
{
|
||||
System.Console.WriteLine(@$"- ""{member}""");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.RichText;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -207,7 +208,10 @@ Suspendisse hendrerit blandit urna ut laoreet. Suspendisse ac elit at erat males
|
||||
private Control TabRichText()
|
||||
{
|
||||
var label = new RichTextLabel();
|
||||
label.SetMessage(FormattedMessage.FromMarkupOrThrow(Lipsum));
|
||||
var msg = FormattedMessage.FromMarkupOrThrow(Lipsum);
|
||||
msg.AddMarkupOrThrow("\n\nWAWWAAWAWWAWA [cmdlink=\"DOES IT WORK\" command=\"help\" /] DOES IT WORK");
|
||||
|
||||
label.SetMessage(msg, [typeof(CommandLinkTag)]);
|
||||
|
||||
TabContainer.SetTabTitle(label, "RichText");
|
||||
return label;
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Robust.Client
|
||||
internal partial class GameController : IPostInjectInit
|
||||
{
|
||||
private IGameLoop? _mainLoop;
|
||||
private bool _dontStart;
|
||||
|
||||
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IDependencyCollection _dependencyCollection = default!;
|
||||
@@ -162,8 +163,11 @@ namespace Robust.Client
|
||||
return;
|
||||
}
|
||||
|
||||
DebugTools.AssertNotNull(_mainLoop);
|
||||
_mainLoop!.Run();
|
||||
if (!_dontStart)
|
||||
{
|
||||
DebugTools.AssertNotNull(_mainLoop);
|
||||
_mainLoop!.Run();
|
||||
}
|
||||
|
||||
CleanupGameThread();
|
||||
}
|
||||
|
||||
@@ -96,6 +96,8 @@ namespace Robust.Client
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
[Dependency] private readonly IReloadManager _reload = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
[Dependency] private readonly ISystemFontManagerInternal _systemFontManager = default!;
|
||||
[Dependency] private readonly LoadingScreenManager _loadscr = default!;
|
||||
|
||||
private IWebViewManagerHook? _webViewHook;
|
||||
|
||||
@@ -132,27 +134,39 @@ namespace Robust.Client
|
||||
return Options.SplashLogo?.ToString() ?? _resourceManifest!.SplashLogo ?? "";
|
||||
}
|
||||
|
||||
public bool ShowLoadingBar()
|
||||
{
|
||||
return _resourceManifest!.ShowLoadingBar ?? _configurationManager.GetCVar(CVars.LoadingShowBar);
|
||||
}
|
||||
|
||||
internal bool StartupContinue(DisplayMode displayMode)
|
||||
{
|
||||
DebugTools.AssertNotNull(_resourceManifest);
|
||||
|
||||
_clyde.InitializePostWindowing();
|
||||
_audio.InitializePostWindowing();
|
||||
_clyde.SetWindowTitle(GameTitle());
|
||||
_loadscr.Initialize(42);
|
||||
|
||||
_taskManager.Initialize();
|
||||
_parallelMgr.Initialize();
|
||||
_loadscr.BeginLoadingSection("Init graphics", dontRender: true);
|
||||
_clyde.InitializePostWindowing();
|
||||
_clyde.SetWindowTitle(GameTitle());
|
||||
_loadscr.EndLoadingSection();
|
||||
|
||||
_loadscr.LoadingStep(_audio.InitializePostWindowing, "Init audio");
|
||||
|
||||
_loadscr.LoadingStep(_taskManager.Initialize, _taskManager);
|
||||
_loadscr.LoadingStep(_parallelMgr.Initialize, _parallelMgr);
|
||||
_fontManager.SetFontDpi((uint)_configurationManager.GetCVar(CVars.DisplayFontDpi));
|
||||
|
||||
// Load optional Robust modules.
|
||||
LoadOptionalRobustModules(displayMode, _resourceManifest!);
|
||||
_loadscr.LoadingStep(_systemFontManager.Initialize, "System fonts");
|
||||
|
||||
// Load optional Robust modules.
|
||||
_loadscr.LoadingStep(() => LoadOptionalRobustModules(displayMode, _resourceManifest!), "Robust Modules");
|
||||
|
||||
_loadscr.BeginLoadingSection(_modLoader);
|
||||
// Disable load context usage on content start.
|
||||
// This prevents Content.Client being loaded twice and things like csi blowing up because of it.
|
||||
_modLoader.SetUseLoadContext(!ContentStart);
|
||||
var disableSandbox = Environment.GetEnvironmentVariable("ROBUST_DISABLE_SANDBOX") == "1";
|
||||
_modLoader.SetEnableSandboxing(!disableSandbox && Options.Sandboxing);
|
||||
|
||||
if (!LoadModules())
|
||||
return false;
|
||||
|
||||
@@ -161,16 +175,23 @@ namespace Robust.Client
|
||||
_configurationManager.LoadCVarsFromAssembly(loadedModule);
|
||||
}
|
||||
|
||||
_serializationManager.Initialize();
|
||||
_loc.Initialize();
|
||||
_loadscr.EndLoadingSection();
|
||||
|
||||
_loadscr.LoadingStep(_serializationManager.Initialize, _serializationManager);
|
||||
_loadscr.LoadingStep(_loc.Initialize, _loc);
|
||||
|
||||
// Call Init in game assemblies.
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.PreInit);
|
||||
_loadscr.LoadingStep(() => _modLoader.BroadcastRunLevel(ModRunLevel.PreInit), "Content PreInit");
|
||||
|
||||
// Finish initialization of WebView if loaded.
|
||||
_webViewHook?.Initialize();
|
||||
_loadscr.LoadingStep(() =>
|
||||
{
|
||||
// Finish initialization of WebView if loaded.
|
||||
if (_webViewHook != null)
|
||||
_loadscr.LoadingStep(_webViewHook.Initialize, _webViewHook);
|
||||
},
|
||||
"WebView init");
|
||||
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.Init);
|
||||
_loadscr.LoadingStep(() => _modLoader.BroadcastRunLevel(ModRunLevel.Init), "Content Init");
|
||||
|
||||
// Start bad file extensions check after content init,
|
||||
// in case content screws with the VFS.
|
||||
@@ -179,42 +200,51 @@ namespace Robust.Client
|
||||
_configurationManager,
|
||||
_logManager.GetSawmill("res"));
|
||||
|
||||
_resourceCache.PreloadTextures();
|
||||
_networkManager.Initialize(false);
|
||||
_configurationManager.SetupNetworking();
|
||||
_serializer.Initialize();
|
||||
_inputManager.Initialize();
|
||||
_console.Initialize();
|
||||
_loadscr.LoadingStep(_resourceCache.PreloadTextures, "Texture preload");
|
||||
_loadscr.LoadingStep(() => { _networkManager.Initialize(false); }, _networkManager);
|
||||
_loadscr.LoadingStep(_configurationManager.SetupNetworking, _configurationManager);
|
||||
_loadscr.LoadingStep(_serializer.Initialize, _serializer);
|
||||
_loadscr.LoadingStep(_inputManager.Initialize, _inputManager);
|
||||
_loadscr.LoadingStep(_console.Initialize, _console);
|
||||
|
||||
// Make sure this is done before we try to load prototypes,
|
||||
// avoid any possibility of race conditions causing the check to not finish
|
||||
// before prototype load.
|
||||
ProgramShared.FinishCheckBadFileExtensions(checkBadExtensions);
|
||||
_loadscr.LoadingStep(
|
||||
() => ProgramShared.FinishCheckBadFileExtensions(checkBadExtensions),
|
||||
"Check bad file extensions");
|
||||
|
||||
_reload.Initialize();
|
||||
_reflectionManager.Initialize();
|
||||
_loadscr.LoadingStep(_reload.Initialize, _reload);
|
||||
_loadscr.LoadingStep(_reflectionManager.Initialize, _reflectionManager);
|
||||
_loadscr.LoadingStep(_xamlProxyManager.Initialize, _xamlProxyManager);
|
||||
_loadscr.LoadingStep(_xamlHotReloadManager.Initialize, _xamlHotReloadManager);
|
||||
_loadscr.BeginLoadingSection(_prototypeManager);
|
||||
_prototypeManager.Initialize();
|
||||
_prototypeManager.LoadDefaultPrototypes();
|
||||
_xamlProxyManager.Initialize();
|
||||
_xamlHotReloadManager.Initialize();
|
||||
_userInterfaceManager.Initialize();
|
||||
_eyeManager.Initialize();
|
||||
_entityManager.Initialize();
|
||||
_mapManager.Initialize();
|
||||
_gameStateManager.Initialize();
|
||||
_placementManager.Initialize();
|
||||
_viewVariablesManager.Initialize();
|
||||
_scriptClient.Initialize();
|
||||
_client.Initialize();
|
||||
_discord.Initialize();
|
||||
_tagManager.Initialize();
|
||||
_protoLoadMan.Initialize();
|
||||
_netResMan.Initialize();
|
||||
_replayLoader.Initialize();
|
||||
_replayPlayback.Initialize();
|
||||
_replayRecording.Initialize();
|
||||
_userInterfaceManager.PostInitialize();
|
||||
_modLoader.BroadcastRunLevel(ModRunLevel.PostInit);
|
||||
_loadscr.EndLoadingSection();
|
||||
_loadscr.LoadingStep(_userInterfaceManager.Initialize, "UI init");
|
||||
_loadscr.LoadingStep(_eyeManager.Initialize, _eyeManager);
|
||||
_loadscr.LoadingStep(_entityManager.Initialize, _entityManager);
|
||||
_loadscr.LoadingStep(_mapManager.Initialize, _mapManager);
|
||||
_loadscr.LoadingStep(_gameStateManager.Initialize, _gameStateManager);
|
||||
_loadscr.LoadingStep(_placementManager.Initialize, _placementManager);
|
||||
_loadscr.LoadingStep(_viewVariablesManager.Initialize, _viewVariablesManager);
|
||||
_loadscr.LoadingStep(_scriptClient.Initialize, _scriptClient);
|
||||
_loadscr.LoadingStep(_client.Initialize, _client);
|
||||
_loadscr.LoadingStep(_discord.Initialize, _discord);
|
||||
_loadscr.LoadingStep(_tagManager.Initialize, _tagManager);
|
||||
_loadscr.LoadingStep(_protoLoadMan.Initialize, _protoLoadMan);
|
||||
_loadscr.LoadingStep(_netResMan.Initialize, _netResMan);
|
||||
_loadscr.LoadingStep(_replayLoader.Initialize, _replayLoader);
|
||||
_loadscr.LoadingStep(_replayPlayback.Initialize, _replayPlayback);
|
||||
_loadscr.LoadingStep(_replayRecording.Initialize, _replayRecording);
|
||||
_loadscr.LoadingStep(_userInterfaceManager.PostInitialize, "UI postinit");
|
||||
|
||||
// Init stuff before this if at all possible.
|
||||
|
||||
_loadscr.LoadingStep(() => _modLoader.BroadcastRunLevel(ModRunLevel.PostInit), "Content PostInit");
|
||||
|
||||
_loadscr.Finish();
|
||||
|
||||
if (_commandLineArgs?.Username != null)
|
||||
{
|
||||
@@ -358,9 +388,6 @@ namespace Robust.Client
|
||||
var userDataDir = GetUserDataDir();
|
||||
|
||||
_configurationManager.Initialize(false);
|
||||
|
||||
// MUST load cvars before loading from config file so the cfg manager is aware of secure cvars.
|
||||
// So SECURE CVars are blacklisted from config.
|
||||
_configurationManager.LoadCVarsFromAssembly(typeof(GameController).Assembly); // Client
|
||||
_configurationManager.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly); // Shared
|
||||
|
||||
@@ -424,7 +451,8 @@ namespace Robust.Client
|
||||
_configurationManager.OverrideConVars(new[]
|
||||
{
|
||||
(CVars.DisplayWindowIconSet.Name, WindowIconSet()),
|
||||
(CVars.DisplaySplashLogo.Name, SplashLogo())
|
||||
(CVars.DisplaySplashLogo.Name, SplashLogo()),
|
||||
(CVars.LoadingShowBar.Name, ShowLoadingBar().ToString()),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -489,10 +517,18 @@ namespace Robust.Client
|
||||
|
||||
public void Shutdown(string? reason = null)
|
||||
{
|
||||
DebugTools.AssertNotNull(_mainLoop);
|
||||
if (_mainLoop == null)
|
||||
{
|
||||
if (!_dontStart)
|
||||
{
|
||||
_logger.Info($"Shutdown called before client init completed: {reason ?? "No reason provided"}");
|
||||
_dontStart = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Already got shut down I assume,
|
||||
if (!_mainLoop!.Running)
|
||||
if (!_mainLoop.Running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -216,35 +216,36 @@ namespace Robust.Client.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
using (histogram?.WithLabels("PredictedQueueDel").NewTimer())
|
||||
base.TickUpdate(frameTime, noPredictions, histogram);
|
||||
}
|
||||
|
||||
internal override void ProcessQueueudDeletions()
|
||||
{
|
||||
base.ProcessQueueudDeletions();
|
||||
while (_queuedPredictedDeletions.TryDequeue(out var uid))
|
||||
{
|
||||
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())
|
||||
{
|
||||
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);
|
||||
}
|
||||
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 Is actually needed after the current predicted deletion fix?
|
||||
base.Dirty(uid, xform, meta);
|
||||
}
|
||||
|
||||
_queuedPredictedDeletionsSet.Clear();
|
||||
}
|
||||
|
||||
base.TickUpdate(frameTime, noPredictions, histogram);
|
||||
_queuedPredictedDeletionsSet.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -361,6 +361,9 @@ namespace Robust.Client.GameStates
|
||||
// avoid exception spam from repeatedly trying to reset the same entity.
|
||||
_entitySystemManager.GetEntitySystem<ClientDirtySystem>().Reset();
|
||||
_runtimeLog.LogException(e, "ResetPredictedEntities");
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
|
||||
// If we were waiting for a new state, we are now applying it.
|
||||
@@ -541,6 +544,11 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
((IBroadcastEventBusInternal)_entities.EventBus).ProcessEventQueue();
|
||||
}
|
||||
|
||||
using (_prof.Group("QueueDel"))
|
||||
{
|
||||
_entities.ProcessQueueudDeletions();
|
||||
}
|
||||
}
|
||||
|
||||
_prof.WriteGroupEnd(groupStart, "Prediction tick", ProfData.Int64(_timing.CurTick.Value));
|
||||
@@ -949,6 +957,9 @@ namespace Robust.Client.GameStates
|
||||
{
|
||||
_sawmill.Error($"Caught exception while deleting entities");
|
||||
_runtimeLog.LogException(e, $"{nameof(ClientGameStateManager)}.{nameof(ApplyEntityStates)}");
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,9 +76,9 @@ namespace Robust.Client.Graphics.Clyde
|
||||
}
|
||||
|
||||
// Short path to render only the splash.
|
||||
if (_drawingSplash)
|
||||
if (_drawingLoadingScreen)
|
||||
{
|
||||
DrawSplash(_renderHandle);
|
||||
DrawLoadingScreen(_renderHandle);
|
||||
FlushRenderQueue();
|
||||
SwapAllBuffers();
|
||||
return;
|
||||
@@ -430,18 +430,11 @@ namespace Robust.Client.Graphics.Clyde
|
||||
FlushRenderQueue();
|
||||
}
|
||||
|
||||
private void DrawSplash(IRenderHandle handle)
|
||||
private void DrawLoadingScreen(IRenderHandle handle)
|
||||
{
|
||||
// Clear screen to black for splash.
|
||||
ClearFramebuffer(Color.Black);
|
||||
|
||||
var splashTex = _cfg.GetCVar(CVars.DisplaySplashLogo);
|
||||
if (string.IsNullOrEmpty(splashTex))
|
||||
return;
|
||||
|
||||
var texture = _resourceCache.GetResource<TextureResource>(splashTex).Texture;
|
||||
|
||||
handle.DrawingHandleScreen.DrawTexture(texture, (ScreenSize - texture.Size) / 2);
|
||||
_loadingScreenManager.DrawLoadingScreen(handle, ScreenSize);
|
||||
}
|
||||
|
||||
private void RenderInRenderTarget(RenderTargetBase rt, Action a, Color? clearColor=default)
|
||||
|
||||
@@ -161,47 +161,33 @@ internal partial class Clyde
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is effectively a specialized combination of a <see cref="Matrix3.TransformBox(in Box2Rotated)"/> and <see cref="Box2Rotated.CalcBoundingBox()"/>.
|
||||
/// This is effectively a specialized combination of a <see cref="Matrix3Helpers.TransformBox(Matrix3x2, in Box2)"/>.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static unsafe Box2 TransformCenteredBox(in Box2 box, float angle, in Vector2 offset, in Vector2 scale)
|
||||
internal static unsafe Box2 TransformCenteredBox(in Box2 box, float angle, in Vector2 offset, in Vector2 scale)
|
||||
{
|
||||
// This function is for sprites, which flip the y axis, so here we flip the definition of t and b relative to the normal function.
|
||||
DebugTools.Assert(scale.Y < 0);
|
||||
|
||||
var boxVec = Unsafe.As<Box2, Vector128<float>>(ref Unsafe.AsRef(in box));
|
||||
|
||||
var sin = Vector128.Create(MathF.Sin(angle));
|
||||
var cos = Vector128.Create(MathF.Cos(angle));
|
||||
var allX = Vector128.Shuffle(boxVec, Vector128.Create(0, 0, 2, 2));
|
||||
var allY = Vector128.Shuffle(boxVec, Vector128.Create(1, 3, 3, 1));
|
||||
var modX = allX * cos - allY * sin;
|
||||
var modY = allX * sin + allY * cos;
|
||||
var boxX = Vector128.Shuffle(boxVec, Vector128.Create(0, 0, 2, 2));
|
||||
var boxY = Vector128.Shuffle(boxVec, Vector128.Create(1, 3, 3, 1));
|
||||
|
||||
var x = boxX * cos - boxY * sin;
|
||||
var y = boxX * sin + boxY * cos;
|
||||
var lbrt = SimdHelpers.GetAABB(x, y);
|
||||
|
||||
// This function is for sprites, which flip the y-axis via the scale, so we need to flip t & b.
|
||||
DebugTools.Assert(scale.Y < 0);
|
||||
lbrt = Vector128.Shuffle(lbrt, Vector128.Create(0,3,2,1));
|
||||
|
||||
var offsetVec = Unsafe.As<Vector2, Vector128<float>>(ref Unsafe.AsRef(in offset)); // upper undefined
|
||||
var scaleVec = Unsafe.As<Vector2, Vector128<float>>(ref Unsafe.AsRef(in scale)); // upper undefined
|
||||
offsetVec = Vector128.Shuffle(offsetVec, Vector128.Create(0, 1, 0, 1));
|
||||
scaleVec = Vector128.Shuffle(scaleVec, Vector128.Create(0, 1, 0, 1));
|
||||
|
||||
Vector128<float> lbrt;
|
||||
if (Sse.IsSupported)
|
||||
{
|
||||
var lrlr = SimdHelpers.MinMaxHorizontalSse(modX);
|
||||
var btbt = SimdHelpers.MaxMinHorizontalSse(modY);
|
||||
lbrt = Sse.UnpackLow(lrlr, btbt);
|
||||
}
|
||||
else
|
||||
{
|
||||
var l = SimdHelpers.MinHorizontal128(allX);
|
||||
var b = SimdHelpers.MaxHorizontal128(allY);
|
||||
var r = SimdHelpers.MaxHorizontal128(allX);
|
||||
var t = SimdHelpers.MinHorizontal128(allY);
|
||||
lbrt = SimdHelpers.MergeRows128(l, b, r, t);
|
||||
}
|
||||
|
||||
// offset and scale box.
|
||||
// note that the scaling here is scaling the whole space, not jut the box. I.e., the centre of the box is changing
|
||||
lbrt = (lbrt + offsetVec) * scaleVec;
|
||||
|
||||
return Unsafe.As<Vector128<float>, Box2>(ref lbrt);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
private long _nextViewportId = 1;
|
||||
|
||||
private readonly ConcurrentQueue<ViewportDisposeData> _viewportDisposeQueue = new();
|
||||
private readonly ConcurrentQueue<(string? name, ViewportDisposeData data)> _viewportDisposeQueue = new();
|
||||
|
||||
private Viewport CreateViewport(Vector2i size, TextureSampleParameters? sampleParameters = default, string? name = null)
|
||||
{
|
||||
@@ -66,13 +66,14 @@ namespace Robust.Client.Graphics.Clyde
|
||||
{
|
||||
while (_viewportDisposeQueue.TryDequeue(out var data))
|
||||
{
|
||||
DisposeViewport(data);
|
||||
DisposeViewport(data.data, data.name, wasLeaked: true);
|
||||
}
|
||||
}
|
||||
|
||||
private void DisposeViewport(ViewportDisposeData disposeData)
|
||||
private void DisposeViewport(ViewportDisposeData disposeData, string? name = null, bool wasLeaked = false)
|
||||
{
|
||||
_clydeSawmill.Warning($"Viewport {disposeData.Id} got leaked");
|
||||
if (wasLeaked)
|
||||
_clydeSawmill.Warning($"Viewport {disposeData.Id} ({name ?? "null"}) got leaked");
|
||||
|
||||
_viewports.Remove(disposeData.Handle);
|
||||
if (disposeData.ClearEvent is not { } clearEvent)
|
||||
@@ -211,7 +212,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
~Viewport()
|
||||
{
|
||||
_clyde._viewportDisposeQueue.Enqueue(DisposeData(referenceSelf: false));
|
||||
_clyde._viewportDisposeQueue.Enqueue((Name, DisposeData(referenceSelf: false)));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -224,7 +225,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
WallBleedIntermediateRenderTarget1.Dispose();
|
||||
WallBleedIntermediateRenderTarget2.Dispose();
|
||||
|
||||
_clyde.DisposeViewport(DisposeData(referenceSelf: false));
|
||||
_clyde.DisposeViewport(DisposeData(referenceSelf: false), Name);
|
||||
}
|
||||
|
||||
private ViewportDisposeData DisposeData(bool referenceSelf)
|
||||
|
||||
@@ -119,8 +119,8 @@ namespace Robust.Client.Graphics.Clyde
|
||||
break;
|
||||
default:
|
||||
_logManager.GetSawmill("clyde.win").Log(
|
||||
LogLevel.Error, "Unknown windowing API: {name}. Falling back to GLFW.", windowingApi);
|
||||
goto case "glfw";
|
||||
LogLevel.Error, "Unknown windowing API: {name}. Falling back to SDL3.", windowingApi);
|
||||
goto case "sdl3";
|
||||
}
|
||||
|
||||
_windowing = winImpl;
|
||||
|
||||
@@ -52,6 +52,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
[Dependency] private readonly ClientEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IReloadManager _reloads = default!;
|
||||
[Dependency] private readonly LoadingScreenManager _loadingScreenManager = default!;
|
||||
|
||||
private GLUniformBuffer<ProjViewMatrices> ProjViewUBO = default!;
|
||||
private GLUniformBuffer<UniformConstants> UniformConstantsUBO = default!;
|
||||
@@ -68,7 +69,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
// VAO is per-window and not stored (not necessary!)
|
||||
private GLBuffer WindowVBO = default!;
|
||||
|
||||
private bool _drawingSplash = true;
|
||||
private bool _drawingLoadingScreen = true;
|
||||
|
||||
private GLShaderProgram? _currentProgram;
|
||||
|
||||
@@ -213,7 +214,7 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
public void Ready()
|
||||
{
|
||||
_drawingSplash = false;
|
||||
_drawingLoadingScreen = false;
|
||||
|
||||
InitLighting();
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ internal partial class Clyde
|
||||
MapKey(SC.SDL_SCANCODE_RALT, Key.Alt);
|
||||
MapKey(SC.SDL_SCANCODE_LGUI, Key.LSystem);
|
||||
MapKey(SC.SDL_SCANCODE_RGUI, Key.RSystem);
|
||||
MapKey(SC.SDL_SCANCODE_MENU, Key.Menu);
|
||||
MapKey(SC.SDL_SCANCODE_APPLICATION, Key.Menu);
|
||||
MapKey(SC.SDL_SCANCODE_LEFTBRACKET, Key.LBracket);
|
||||
MapKey(SC.SDL_SCANCODE_RIGHTBRACKET, Key.RBracket);
|
||||
MapKey(SC.SDL_SCANCODE_SEMICOLON, Key.SemiColon);
|
||||
@@ -173,7 +173,7 @@ internal partial class Clyde
|
||||
MapKey(SC.SDL_SCANCODE_KP_MINUS, Key.NumpadSubtract);
|
||||
MapKey(SC.SDL_SCANCODE_KP_DIVIDE, Key.NumpadDivide);
|
||||
MapKey(SC.SDL_SCANCODE_KP_MULTIPLY, Key.NumpadMultiply);
|
||||
MapKey(SC.SDL_SCANCODE_KP_DECIMAL, Key.NumpadDecimal);
|
||||
MapKey(SC.SDL_SCANCODE_KP_PERIOD, Key.NumpadDecimal);
|
||||
MapKey(SC.SDL_SCANCODE_LEFT, Key.Left);
|
||||
MapKey(SC.SDL_SCANCODE_RIGHT, Key.Right);
|
||||
MapKey(SC.SDL_SCANCODE_UP, Key.Up);
|
||||
|
||||
@@ -104,6 +104,12 @@ namespace Robust.Client.Graphics
|
||||
Handle = IoCManager.Resolve<IFontManagerInternal>().MakeInstance(res.FontFaceHandle, size);
|
||||
}
|
||||
|
||||
internal VectorFont(IFontInstanceHandle handle, int size)
|
||||
{
|
||||
Size = size;
|
||||
Handle = handle;
|
||||
}
|
||||
|
||||
public override int GetAscent(float scale) => Handle.GetAscent(scale);
|
||||
public override int GetHeight(float scale) => Handle.GetHeight(scale);
|
||||
public override int GetDescent(float scale) => Handle.GetDescent(scale);
|
||||
@@ -222,4 +228,74 @@ namespace Robust.Client.Graphics
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible values for font weights. Larger values have thicker font strokes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These values are based on the <c>usWeightClass</c> property of the OpenType specification:
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="ISystemFontFace.Weight"/>
|
||||
public enum FontWeight : ushort
|
||||
{
|
||||
Thin = 100,
|
||||
ExtraLight = 200,
|
||||
UltraLight = ExtraLight,
|
||||
Light = 300,
|
||||
SemiLight = 350,
|
||||
Normal = 400,
|
||||
Regular = Normal,
|
||||
Medium = 500,
|
||||
SemiBold = 600,
|
||||
DemiBold = SemiBold,
|
||||
Bold = 700,
|
||||
ExtraBold = 800,
|
||||
UltraBold = ExtraBold,
|
||||
Black = 900,
|
||||
Heavy = Black,
|
||||
ExtraBlack = 950,
|
||||
UltraBlack = ExtraBlack,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible slant values for fonts.
|
||||
/// </summary>
|
||||
/// <seealso cref="ISystemFontFace.Slant"/>
|
||||
public enum FontSlant : byte
|
||||
{
|
||||
// NOTE: Enum values correspond to DWRITE_FONT_STYLE.
|
||||
Normal = 0,
|
||||
Oblique = 1,
|
||||
|
||||
// FUN FACT: they're called "italics" because they look like the Leaning Tower of Pisa.
|
||||
// Don't fact-check that.
|
||||
Italic = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible values for font widths. Larger values are proportionally wider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// These values are based on the <c>usWidthClass</c> property of the OpenType specification:
|
||||
/// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="ISystemFontFace.Width"/>
|
||||
public enum FontWidth : ushort
|
||||
{
|
||||
UltraCondensed = 1,
|
||||
ExtraCondensed = 2,
|
||||
Condensed = 3,
|
||||
SemiCondensed = 4,
|
||||
Normal = 5,
|
||||
Medium = Normal,
|
||||
SemiExpanded = 6,
|
||||
Expanded = 7,
|
||||
ExtraExpanded = 8,
|
||||
UltraExpanded = 9,
|
||||
}
|
||||
}
|
||||
|
||||
15
Robust.Client/Graphics/FontManagement/SystemFontDebug.cs
Normal file
15
Robust.Client/Graphics/FontManagement/SystemFontDebug.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Robust.Client.Graphics.FontManagement;
|
||||
|
||||
internal sealed class SystemFontDebugCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "system_font_debug";
|
||||
public string Description => "";
|
||||
public string Help => "";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
new SystemFontDebugWindow().OpenCentered();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
Title="System font debug">
|
||||
<SplitContainer Orientation="Horizontal" MinSize="800 600">
|
||||
<ScrollContainer HScrollEnabled="False">
|
||||
<BoxContainer Name="SelectorContainer" Orientation="Vertical" />
|
||||
</ScrollContainer>
|
||||
<ScrollContainer HScrollEnabled="False">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Name="FamilyLabel" />
|
||||
<BoxContainer Orientation="Vertical" Name="FaceContainer" />
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</SplitContainer>
|
||||
</DefaultWindow>
|
||||
@@ -0,0 +1,98 @@
|
||||
using System.Linq;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Graphics.FontManagement;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
internal sealed partial class SystemFontDebugWindow : DefaultWindow
|
||||
{
|
||||
private static readonly int[] ExampleFontSizes = [8, 12, 16, 24, 36];
|
||||
private const string ExampleString = "The quick brown fox jumps over the lazy dog";
|
||||
|
||||
[Dependency] private readonly ISystemFontManager _systemFontManager = default!;
|
||||
|
||||
public SystemFontDebugWindow()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var buttonGroup = new ButtonGroup();
|
||||
|
||||
foreach (var group in _systemFontManager.SystemFontFaces.GroupBy(k => k.FamilyName).OrderBy(k => k.Key))
|
||||
{
|
||||
var fonts = group.ToArray();
|
||||
SelectorContainer.AddChild(new Selector(this, buttonGroup, group.Key, fonts));
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectFontFamily(ISystemFontFace[] fonts)
|
||||
{
|
||||
FamilyLabel.Text = fonts[0].FamilyName;
|
||||
|
||||
FaceContainer.RemoveAllChildren();
|
||||
|
||||
foreach (var font in fonts)
|
||||
{
|
||||
var exampleContainer = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Margin = new Thickness(8)
|
||||
};
|
||||
|
||||
foreach (var size in ExampleFontSizes)
|
||||
{
|
||||
var fontInstance = font.Load(size);
|
||||
|
||||
var richTextLabel = new RichTextLabel
|
||||
{
|
||||
Stylesheet = new Stylesheet([
|
||||
StylesheetHelpers.Element<RichTextLabel>().Prop("font", fontInstance)
|
||||
]),
|
||||
};
|
||||
richTextLabel.SetMessage(FormattedMessage.FromUnformatted(ExampleString));
|
||||
exampleContainer.AddChild(richTextLabel);
|
||||
}
|
||||
|
||||
FaceContainer.AddChild(new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Children =
|
||||
{
|
||||
new RichTextLabel
|
||||
{
|
||||
Text = $"""
|
||||
{font.FullName}
|
||||
Family: "{font.FamilyName}", face: "{font.FaceName}", PostScript = "{font.PostscriptName}"
|
||||
Weight: {font.Weight} ({(int) font.Weight}), slant: {font.Slant} ({(int) font.Slant}), width: {font.Width} ({(int) font.Width})
|
||||
""",
|
||||
},
|
||||
exampleContainer
|
||||
},
|
||||
Margin = new Thickness(0, 0, 0, 8)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class Selector : Control
|
||||
{
|
||||
public Selector(SystemFontDebugWindow window, ButtonGroup group, string family, ISystemFontFace[] fonts)
|
||||
{
|
||||
var button = new Button
|
||||
{
|
||||
Text = family,
|
||||
Group = group,
|
||||
ToggleMode = true
|
||||
};
|
||||
AddChild(button);
|
||||
|
||||
button.OnPressed += _ => window.SelectFontFamily(fonts);
|
||||
}
|
||||
}
|
||||
}
|
||||
170
Robust.Client/Graphics/FontManagement/SystemFontManagerBase.cs
Normal file
170
Robust.Client/Graphics/FontManagement/SystemFontManagerBase.cs
Normal file
@@ -0,0 +1,170 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Threading;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Robust.Client.Graphics.FontManagement;
|
||||
|
||||
internal abstract class SystemFontManagerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The "standard" locale used when looking up the PostScript name of a font face.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Font files allow the PostScript name to be localized, however in practice
|
||||
/// we would really like to have a language-unambiguous identifier to refer to a font file.
|
||||
/// We use this locale (en-US) to look up teh PostScript font name, if there are multiple provided.
|
||||
/// This matches the behavior of the Local Font Access web API:
|
||||
/// https://wicg.github.io/local-font-access/#concept-font-representation
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected static readonly CultureInfo StandardLocale = new("en-US", false);
|
||||
|
||||
protected readonly IFontManagerInternal FontManager;
|
||||
protected readonly ISawmill Sawmill;
|
||||
|
||||
protected readonly Lock Lock = new();
|
||||
protected readonly List<BaseHandle> Fonts = [];
|
||||
|
||||
public IEnumerable<ISystemFontFace> SystemFontFaces { get; }
|
||||
|
||||
public SystemFontManagerBase(ILogManager logManager, IFontManagerInternal fontManager)
|
||||
{
|
||||
FontManager = fontManager;
|
||||
Sawmill = logManager.GetSawmill("font.system");
|
||||
|
||||
SystemFontFaces = Fonts.AsReadOnly();
|
||||
}
|
||||
|
||||
protected abstract IFontFaceHandle LoadFontFace(BaseHandle handle);
|
||||
|
||||
protected static string GetLocalizedForLocaleOrFirst(LocalizedStringSet set, CultureInfo culture)
|
||||
{
|
||||
var matchCulture = culture;
|
||||
while (!Equals(matchCulture, CultureInfo.InvariantCulture))
|
||||
{
|
||||
if (set.Values.TryGetValue(culture.Name, out var value))
|
||||
return value;
|
||||
|
||||
matchCulture = matchCulture.Parent;
|
||||
}
|
||||
|
||||
return set.Values[set.Primary];
|
||||
}
|
||||
|
||||
protected abstract class BaseHandle(SystemFontManagerBase parent) : ISystemFontFace
|
||||
{
|
||||
private IFontFaceHandle? _cachedFont;
|
||||
|
||||
public required string PostscriptName { get; init; }
|
||||
|
||||
public required LocalizedStringSet FullNames;
|
||||
public required LocalizedStringSet FamilyNames;
|
||||
public required LocalizedStringSet FaceNames;
|
||||
|
||||
public required FontWeight Weight { get; init; }
|
||||
public required FontSlant Slant { get; init; }
|
||||
public required FontWidth Width { get; init; }
|
||||
|
||||
public string FullName => GetLocalizedFullName(CultureInfo.CurrentCulture);
|
||||
public string FamilyName => GetLocalizedFamilyName(CultureInfo.CurrentCulture);
|
||||
public string FaceName => GetLocalizedFaceName(CultureInfo.CurrentCulture);
|
||||
|
||||
public string GetLocalizedFullName(CultureInfo culture)
|
||||
{
|
||||
return GetLocalizedForLocaleOrFirst(FullNames, culture);
|
||||
}
|
||||
|
||||
public string GetLocalizedFamilyName(CultureInfo culture)
|
||||
{
|
||||
return GetLocalizedForLocaleOrFirst(FamilyNames, culture);
|
||||
}
|
||||
|
||||
public string GetLocalizedFaceName(CultureInfo culture)
|
||||
{
|
||||
return GetLocalizedForLocaleOrFirst(FaceNames, culture);
|
||||
}
|
||||
|
||||
public Font Load(int size)
|
||||
{
|
||||
var handle = GetFaceHandle();
|
||||
|
||||
var instance = parent.FontManager.MakeInstance(handle, size);
|
||||
|
||||
return new VectorFont(instance, size);
|
||||
}
|
||||
|
||||
private IFontFaceHandle GetFaceHandle()
|
||||
{
|
||||
lock (parent.Lock)
|
||||
{
|
||||
if (_cachedFont != null)
|
||||
return _cachedFont;
|
||||
|
||||
parent.Sawmill.Verbose($"Loading system font face: {PostscriptName}");
|
||||
|
||||
return _cachedFont = parent.LoadFontFace(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected struct LocalizedStringSet
|
||||
{
|
||||
public static readonly LocalizedStringSet Empty = FromSingle("");
|
||||
|
||||
/// <summary>
|
||||
/// The first locale to appear in the list of localized strings.
|
||||
/// Used as fallback if the desired locale is not provided.
|
||||
/// </summary>
|
||||
public required string Primary;
|
||||
public required Dictionary<string, string> Values;
|
||||
|
||||
public static LocalizedStringSet FromSingle(string value, string language = "en")
|
||||
{
|
||||
return new LocalizedStringSet
|
||||
{
|
||||
Primary = language,
|
||||
Values = new Dictionary<string, string> { { language, value } }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected sealed class MemoryMappedFontMemoryHandle : IFontMemoryHandle
|
||||
{
|
||||
private readonly MemoryMappedFile _mappedFile;
|
||||
private readonly MemoryMappedViewAccessor _accessor;
|
||||
|
||||
public MemoryMappedFontMemoryHandle(string filePath)
|
||||
{
|
||||
_mappedFile = MemoryMappedFile.CreateFromFile(
|
||||
filePath,
|
||||
FileMode.Open,
|
||||
null,
|
||||
0,
|
||||
MemoryMappedFileAccess.Read);
|
||||
|
||||
_accessor = _mappedFile.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read);
|
||||
}
|
||||
|
||||
public unsafe byte* GetData()
|
||||
{
|
||||
byte* pointer = null;
|
||||
_accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer);
|
||||
return pointer;
|
||||
}
|
||||
|
||||
public nint GetDataSize()
|
||||
{
|
||||
return (nint)_accessor.Capacity;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_accessor.Dispose();
|
||||
_mappedFile.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
#if MACOS
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Client.Interop.MacOS;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using CF = Robust.Client.Interop.MacOS.CoreFoundation;
|
||||
using CT = Robust.Client.Interop.MacOS.CoreText;
|
||||
|
||||
namespace Robust.Client.Graphics.FontManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="ISystemFontManager"/> that uses CoreText on macOS.
|
||||
/// </summary>
|
||||
internal sealed class SystemFontManagerCoreText : SystemFontManagerBase, ISystemFontManagerInternal
|
||||
{
|
||||
private static readonly FontWidth[] FontWidths = Enum.GetValues<FontWidth>();
|
||||
|
||||
public bool IsSupported => true;
|
||||
|
||||
public SystemFontManagerCoreText(ILogManager logManager, IFontManagerInternal fontManager) : base(logManager,
|
||||
fontManager)
|
||||
{
|
||||
}
|
||||
|
||||
public unsafe void Initialize()
|
||||
{
|
||||
Sawmill.Verbose("Getting CTFontCollection...");
|
||||
|
||||
var collection = CT.CTFontCollectionCreateFromAvailableFonts(null);
|
||||
var array = CT.CTFontCollectionCreateMatchingFontDescriptors(collection);
|
||||
|
||||
var count = CF.CFArrayGetCount(array);
|
||||
Sawmill.Verbose($"Have {count} descriptors...");
|
||||
|
||||
for (nint i = 0; i < count.Value; i++)
|
||||
{
|
||||
var item = (__CTFontDescriptor*)CF.CFRetain(CF.CFArrayGetValueAtIndex(array, new CLong(i)));
|
||||
|
||||
try
|
||||
{
|
||||
LoadFontDescriptor(item);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Sawmill.Error($"Failed to load font descriptor: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
CF.CFRelease(item);
|
||||
}
|
||||
}
|
||||
|
||||
CF.CFRelease(array);
|
||||
CF.CFRelease(collection);
|
||||
}
|
||||
|
||||
private unsafe void LoadFontDescriptor(__CTFontDescriptor* descriptor)
|
||||
{
|
||||
var displayName = GetFontAttributeManaged(descriptor, CT.kCTFontDisplayNameAttribute);
|
||||
var postscriptName = GetFontAttributeManaged(descriptor, CT.kCTFontNameAttribute);
|
||||
var familyName = GetFontAttributeManaged(descriptor, CT.kCTFontFamilyNameAttribute);
|
||||
var styleName = GetFontAttributeManaged(descriptor, CT.kCTFontStyleNameAttribute);
|
||||
|
||||
var url = (__CFURL*)CT.CTFontDescriptorCopyAttribute(descriptor, CT.kCTFontURLAttribute);
|
||||
|
||||
const int maxPath = 1024;
|
||||
var buf = stackalloc byte[maxPath];
|
||||
var result = CF.CFURLGetFileSystemRepresentation(url, 1, buf, new CLong(maxPath));
|
||||
if (result == 0)
|
||||
throw new Exception("CFURLGetFileSystemRepresentation failed!");
|
||||
|
||||
// Sawmill.Verbose(CF.CFStringToManaged(CF.CFURLGetString(url)));
|
||||
|
||||
CF.CFRelease(url);
|
||||
|
||||
var traits = (__CFDictionary*)CT.CTFontDescriptorCopyAttribute(descriptor, CT.kCTFontTraitsAttribute);
|
||||
var (weight, slant, width) = ParseTraits(traits);
|
||||
|
||||
CF.CFRelease(traits);
|
||||
|
||||
var path = Marshal.PtrToStringUTF8((nint)buf)!;
|
||||
|
||||
Fonts.Add(new Handle(this)
|
||||
{
|
||||
PostscriptName = postscriptName,
|
||||
FullNames = LocalizedStringSet.FromSingle(displayName),
|
||||
FamilyNames = LocalizedStringSet.FromSingle(familyName),
|
||||
FaceNames = LocalizedStringSet.FromSingle(styleName),
|
||||
Weight = weight,
|
||||
Slant = slant,
|
||||
Width = width,
|
||||
Path = path
|
||||
});
|
||||
}
|
||||
|
||||
private static unsafe (FontWeight, FontSlant, FontWidth) ParseTraits(__CFDictionary* dictionary)
|
||||
{
|
||||
var weight = FontWeight.Normal;
|
||||
var slant = FontSlant.Normal;
|
||||
var width = FontWidth.Normal;
|
||||
|
||||
var weightVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontWeightTrait);
|
||||
if (weightVal != null)
|
||||
weight = ConvertWeight(weightVal);
|
||||
|
||||
var slantVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontSlantTrait);
|
||||
if (slantVal != null)
|
||||
slant = ConvertSlant(slantVal);
|
||||
|
||||
var widthVal = (__CFNumber*)CF.CFDictionaryGetValue(dictionary, CT.kCTFontWidthTrait);
|
||||
if (widthVal != null)
|
||||
width = ConvertWidth(widthVal);
|
||||
|
||||
return (weight, slant, width);
|
||||
}
|
||||
|
||||
private static readonly (float, FontWeight)[] FontWeightTable =
|
||||
[
|
||||
((float) AppKit.NSFontWeightUltraLight, FontWeight.UltraLight),
|
||||
((float) AppKit.NSFontWeightThin, FontWeight.Thin),
|
||||
((float) AppKit.NSFontWeightLight, FontWeight.Light),
|
||||
((float) AppKit.NSFontWeightRegular, FontWeight.Regular),
|
||||
((float) AppKit.NSFontWeightMedium, FontWeight.Medium),
|
||||
((float) AppKit.NSFontWeightSemiBold, FontWeight.SemiBold),
|
||||
((float) AppKit.NSFontWeightBold, FontWeight.Bold),
|
||||
((float) AppKit.NSFontWeightHeavy, FontWeight.Heavy),
|
||||
((float) AppKit.NSFontWeightBlack, FontWeight.Black)
|
||||
];
|
||||
|
||||
private static unsafe FontWeight ConvertWeight(__CFNumber* number)
|
||||
{
|
||||
float val;
|
||||
CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val);
|
||||
|
||||
var valCopy = val;
|
||||
return FontWeightTable.MinBy(tup => Math.Abs(tup.Item1 - valCopy)).Item2;
|
||||
}
|
||||
|
||||
private static unsafe FontWidth ConvertWidth(__CFNumber* number)
|
||||
{
|
||||
float val;
|
||||
CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val);
|
||||
|
||||
// Normalize to 0-1 range
|
||||
val = (val + 1) / 2;
|
||||
var lerped = MathHelper.Lerp((float)FontWidths[0], (float)FontWidths[^1], val);
|
||||
return FontWidths.MinBy(x => Math.Abs((float)x - lerped));
|
||||
}
|
||||
|
||||
private static unsafe FontSlant ConvertSlant(__CFNumber* number)
|
||||
{
|
||||
float val;
|
||||
CF.CFNumberGetValue(number, new CLong(CF.kCFNumberFloat32Type), &val);
|
||||
|
||||
// Normalize to 0-1 range
|
||||
return val == 0 ? FontSlant.Normal : FontSlant.Italic;
|
||||
}
|
||||
|
||||
private static unsafe string GetFontAttributeManaged(__CTFontDescriptor* descriptor, __CFString* key)
|
||||
{
|
||||
var str = (__CFString*)CT.CTFontDescriptorCopyAttribute(descriptor, key);
|
||||
|
||||
try
|
||||
{
|
||||
return CF.CFStringToManaged(str);
|
||||
}
|
||||
finally
|
||||
{
|
||||
CF.CFRelease(str);
|
||||
}
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
// Nothing to do.
|
||||
}
|
||||
|
||||
protected override IFontFaceHandle LoadFontFace(BaseHandle handle)
|
||||
{
|
||||
var path = ((Handle)handle).Path;
|
||||
Sawmill.Verbose(path);
|
||||
|
||||
// CTFontDescriptor does not seem to have any way to identify *which* index in the font file should be accessed.
|
||||
// So we have to just load every one until the postscript name matches.
|
||||
return FontManager.LoadWithPostscriptName(new MemoryMappedFontMemoryHandle(path), handle.PostscriptName);
|
||||
}
|
||||
|
||||
private sealed class Handle(SystemFontManagerCoreText parent) : BaseHandle(parent)
|
||||
{
|
||||
public required string Path;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,503 @@
|
||||
#if WINDOWS
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
using TerraFX.Interop.DirectX;
|
||||
using TerraFX.Interop.Windows;
|
||||
using static TerraFX.Interop.DirectX.DWRITE_FACTORY_TYPE;
|
||||
using static TerraFX.Interop.DirectX.DWRITE_FONT_PROPERTY_ID;
|
||||
using static TerraFX.Interop.Windows.Windows;
|
||||
|
||||
namespace Robust.Client.Graphics.FontManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="ISystemFontManager"/> that uses DirectWrite on Windows.
|
||||
/// </summary>
|
||||
internal sealed unsafe class SystemFontManagerDirectWrite : SystemFontManagerBase, ISystemFontManagerInternal
|
||||
{
|
||||
// For future implementors of other platforms:
|
||||
// a significant amount of code in this file will be shareable with that of other platforms,
|
||||
// so some refactoring is warranted.
|
||||
|
||||
private readonly IConfigurationManager _cfg;
|
||||
|
||||
private IDWriteFactory3* _dWriteFactory;
|
||||
private IDWriteFontSet* _systemFontSet;
|
||||
|
||||
public bool IsSupported => true;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="ISystemFontManager"/> that uses DirectWrite on Windows.
|
||||
/// </summary>
|
||||
public SystemFontManagerDirectWrite(
|
||||
ILogManager logManager,
|
||||
IConfigurationManager cfg,
|
||||
IFontManagerInternal fontManager)
|
||||
: base(logManager, fontManager)
|
||||
{
|
||||
_cfg = cfg;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
CreateDWriteFactory();
|
||||
|
||||
_systemFontSet = GetSystemFontSet(_dWriteFactory);
|
||||
|
||||
lock (Lock)
|
||||
{
|
||||
var fontCount = _systemFontSet->GetFontCount();
|
||||
for (var i = 0u; i < fontCount; i++)
|
||||
{
|
||||
LoadSingleFontFromSet(_systemFontSet, i);
|
||||
}
|
||||
}
|
||||
|
||||
Sawmill.Verbose($"Loaded {Fonts.Count} fonts");
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_systemFontSet->Release();
|
||||
_systemFontSet = null;
|
||||
|
||||
_dWriteFactory->Release();
|
||||
_dWriteFactory = null;
|
||||
|
||||
lock (Lock)
|
||||
{
|
||||
foreach (var systemFont in Fonts)
|
||||
{
|
||||
((Handle)systemFont).FontFace->Release();
|
||||
}
|
||||
|
||||
Fonts.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSingleFontFromSet(IDWriteFontSet* set, uint fontIndex)
|
||||
{
|
||||
// Get basic parameters that every font should probably have?
|
||||
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_POSTSCRIPT_NAME, out var postscriptNames))
|
||||
return;
|
||||
|
||||
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FULL_NAME, out var fullNames))
|
||||
return;
|
||||
|
||||
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FAMILY_NAME, out var familyNames))
|
||||
return;
|
||||
|
||||
if (!TryGetStringsSet(set, fontIndex, DWRITE_FONT_PROPERTY_ID_FACE_NAME, out var faceNames))
|
||||
return;
|
||||
|
||||
// I assume these parameters can't be missing in practice, but better safe than sorry.
|
||||
TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_WEIGHT, out var weight);
|
||||
TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_STYLE, out var style);
|
||||
TryGetStrings(set, fontIndex, DWRITE_FONT_PROPERTY_ID_STRETCH, out var stretch);
|
||||
|
||||
var parsedWeight = ParseFontWeight(weight);
|
||||
var parsedSlant = ParseFontSlant(style);
|
||||
var parsedWidth = ParseFontWidth(stretch);
|
||||
|
||||
IDWriteFontFaceReference* reference = null;
|
||||
var result = set->GetFontFaceReference(fontIndex, &reference);
|
||||
ThrowIfFailed(result);
|
||||
|
||||
var handle = new Handle(this, reference)
|
||||
{
|
||||
PostscriptName = GetLocalizedForLocaleOrFirst(postscriptNames, StandardLocale),
|
||||
FullNames = fullNames,
|
||||
FamilyNames = familyNames,
|
||||
FaceNames = faceNames,
|
||||
Weight = parsedWeight,
|
||||
Slant = parsedSlant,
|
||||
Width = parsedWidth
|
||||
};
|
||||
|
||||
Fonts.Add(handle);
|
||||
}
|
||||
|
||||
private static FontWeight ParseFontWeight(DWriteLocalizedString[]? strings)
|
||||
{
|
||||
if (strings == null)
|
||||
return FontWeight.Regular;
|
||||
|
||||
return (FontWeight)Parse.Int32(strings[0].Value);
|
||||
}
|
||||
|
||||
private static FontSlant ParseFontSlant(DWriteLocalizedString[]? strings)
|
||||
{
|
||||
if (strings == null)
|
||||
return FontSlant.Normal;
|
||||
|
||||
return (FontSlant)Parse.Int32(strings[0].Value);
|
||||
}
|
||||
|
||||
private static FontWidth ParseFontWidth(DWriteLocalizedString[]? strings)
|
||||
{
|
||||
if (strings == null)
|
||||
return FontWidth.Normal;
|
||||
|
||||
return (FontWidth)Parse.Int32(strings[0].Value);
|
||||
}
|
||||
|
||||
private void CreateDWriteFactory()
|
||||
{
|
||||
fixed (IDWriteFactory3** pFactory = &_dWriteFactory)
|
||||
{
|
||||
var result = DirectX.DWriteCreateFactory(
|
||||
DWRITE_FACTORY_TYPE_SHARED,
|
||||
__uuidof<IDWriteFactory3>(),
|
||||
(IUnknown**)pFactory);
|
||||
|
||||
ThrowIfFailed(result);
|
||||
}
|
||||
}
|
||||
|
||||
private IDWriteFontSet* GetSystemFontSet(IDWriteFactory3* factory)
|
||||
{
|
||||
IDWriteFactory6* factory6;
|
||||
IDWriteFontSet* fontSet;
|
||||
var result = factory->QueryInterface(__uuidof<IDWriteFactory6>(), (void**)&factory6);
|
||||
if (result.SUCCEEDED)
|
||||
{
|
||||
Sawmill.Verbose("IDWriteFactory6 available, using newer GetSystemFontSet");
|
||||
|
||||
result = factory6->GetSystemFontSet(
|
||||
_cfg.GetCVar(CVars.FontWindowsDownloadable),
|
||||
(IDWriteFontSet1**)(&fontSet));
|
||||
|
||||
factory6->Release();
|
||||
}
|
||||
else
|
||||
{
|
||||
Sawmill.Verbose("IDWriteFactory6 not available");
|
||||
|
||||
result = factory->GetSystemFontSet(&fontSet);
|
||||
}
|
||||
|
||||
ThrowIfFailed(result, "GetSystemFontSet");
|
||||
return fontSet;
|
||||
}
|
||||
|
||||
protected override IFontFaceHandle LoadFontFace(BaseHandle handle)
|
||||
{
|
||||
var fontFace = ((Handle)handle).FontFace;
|
||||
IDWriteFontFile* file = null;
|
||||
IDWriteFontFileLoader* loader = null;
|
||||
|
||||
try
|
||||
{
|
||||
var result = fontFace->GetFontFile(&file);
|
||||
ThrowIfFailed(result, "IDWriteFontFaceReference::GetFontFile");
|
||||
result = file->GetLoader(&loader);
|
||||
ThrowIfFailed(result, "IDWriteFontFile::GetLoader");
|
||||
|
||||
void* referenceKey;
|
||||
uint referenceKeyLength;
|
||||
result = file->GetReferenceKey(&referenceKey, &referenceKeyLength);
|
||||
ThrowIfFailed(result, "IDWriteFontFile::GetReferenceKey");
|
||||
|
||||
IDWriteLocalFontFileLoader* localLoader;
|
||||
result = loader->QueryInterface(__uuidof<IDWriteLocalFontFileLoader>(), (void**)&localLoader);
|
||||
if (result.SUCCEEDED)
|
||||
{
|
||||
Sawmill.Verbose("Loading font face via memory mapped file...");
|
||||
|
||||
// We can get the local file path on disk. This means we can directly load it via mmap.
|
||||
uint filePathLength;
|
||||
ThrowIfFailed(
|
||||
localLoader->GetFilePathLengthFromKey(referenceKey, referenceKeyLength, &filePathLength),
|
||||
"IDWriteLocalFontFileLoader::GetFilePathLengthFromKey");
|
||||
var filePath = new char[filePathLength + 1];
|
||||
fixed (char* pFilePath = filePath)
|
||||
{
|
||||
ThrowIfFailed(
|
||||
localLoader->GetFilePathFromKey(
|
||||
referenceKey,
|
||||
referenceKeyLength,
|
||||
pFilePath,
|
||||
(uint)filePath.Length),
|
||||
"IDWriteLocalFontFileLoader::GetFilePathFromKey");
|
||||
}
|
||||
|
||||
var path = new string(filePath, 0, (int)filePathLength);
|
||||
|
||||
localLoader->Release();
|
||||
|
||||
return FontManager.Load(new MemoryMappedFontMemoryHandle(path));
|
||||
}
|
||||
else
|
||||
{
|
||||
Sawmill.Verbose("Loading font face via stream...");
|
||||
|
||||
// DirectWrite doesn't give us anything to go with for this file, read it into regular memory.
|
||||
// If the font file has multiple faces, which is possible, then this approach will duplicate memory.
|
||||
// That sucks, but I'm really not sure whether there's any way around this short of
|
||||
// comparing the memory contents by hashing to check equality.
|
||||
// As I'm pretty sure we can't like reference equality check the font objects somehow.
|
||||
IDWriteFontFileStream* stream;
|
||||
result = loader->CreateStreamFromKey(referenceKey, referenceKeyLength, &stream);
|
||||
ThrowIfFailed(result, "IDWriteFontFileLoader::CreateStreamFromKey");
|
||||
|
||||
using var streamObject = new DirectWriteStream(stream);
|
||||
return FontManager.Load(streamObject, (int)fontFace->GetFontFaceIndex());
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (file != null)
|
||||
file->Release();
|
||||
if (loader != null)
|
||||
loader->Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetStrings(
|
||||
IDWriteFontSet* set,
|
||||
uint listIndex,
|
||||
DWRITE_FONT_PROPERTY_ID property,
|
||||
[NotNullWhen(true)] out DWriteLocalizedString[]? strings)
|
||||
{
|
||||
BOOL exists;
|
||||
IDWriteLocalizedStrings* dWriteStrings = null;
|
||||
var result = set->GetPropertyValues(
|
||||
listIndex,
|
||||
property,
|
||||
&exists,
|
||||
&dWriteStrings);
|
||||
ThrowIfFailed(result, "IDWriteFontSet::GetPropertyValues");
|
||||
|
||||
if (!exists)
|
||||
{
|
||||
strings = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
strings = GetStrings(dWriteStrings);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
dWriteStrings->Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetStringsSet(
|
||||
IDWriteFontSet* set,
|
||||
uint listIndex,
|
||||
DWRITE_FONT_PROPERTY_ID property,
|
||||
out LocalizedStringSet strings)
|
||||
{
|
||||
if (!TryGetStrings(set, listIndex, property, out var stringsArray))
|
||||
{
|
||||
strings = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
strings = StringsToSet(stringsArray);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static DWriteLocalizedString[] GetStrings(IDWriteLocalizedStrings* localizedStrings)
|
||||
{
|
||||
IDWriteStringList* list;
|
||||
ThrowIfFailed(localizedStrings->QueryInterface(__uuidof<IDWriteStringList>(), (void**)&list));
|
||||
|
||||
try
|
||||
{
|
||||
return GetStrings(list);
|
||||
}
|
||||
finally
|
||||
{
|
||||
list->Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static DWriteLocalizedString[] GetStrings(IDWriteStringList* stringList)
|
||||
{
|
||||
var array = new DWriteLocalizedString[stringList->GetCount()];
|
||||
|
||||
var stringPool = ArrayPool<char>.Shared.Rent(256);
|
||||
|
||||
for (var i = 0; i < array.Length; i++)
|
||||
{
|
||||
uint length;
|
||||
|
||||
ThrowIfFailed(stringList->GetStringLength((uint)i, &length), "IDWriteStringList::GetStringLength");
|
||||
ExpandIfNecessary(ref stringPool, length + 1);
|
||||
fixed (char* pArr = stringPool)
|
||||
{
|
||||
ThrowIfFailed(
|
||||
stringList->GetString((uint)i, pArr, (uint)stringPool.Length),
|
||||
"IDWriteStringList::GetString");
|
||||
}
|
||||
|
||||
var value = new string(stringPool, 0, (int)length);
|
||||
|
||||
ThrowIfFailed(stringList->GetLocaleNameLength((uint)i, &length), "IDWriteStringList::GetLocaleNameLength");
|
||||
ExpandIfNecessary(ref stringPool, length + 1);
|
||||
fixed (char* pArr = stringPool)
|
||||
{
|
||||
ThrowIfFailed(
|
||||
stringList->GetLocaleName((uint)i, pArr, (uint)stringPool.Length),
|
||||
"IDWriteStringList::GetLocaleName");
|
||||
}
|
||||
|
||||
var localeName = new string(stringPool, 0, (int)length);
|
||||
|
||||
array[i] = new DWriteLocalizedString(value, localeName);
|
||||
}
|
||||
|
||||
ArrayPool<char>.Shared.Return(stringPool);
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
private static void ExpandIfNecessary(ref char[] array, uint requiredLength)
|
||||
{
|
||||
if (requiredLength < array.Length)
|
||||
return;
|
||||
|
||||
ArrayPool<char>.Shared.Return(array);
|
||||
array = ArrayPool<char>.Shared.Rent(checked((int)requiredLength));
|
||||
}
|
||||
|
||||
private static LocalizedStringSet StringsToSet(DWriteLocalizedString[] strings)
|
||||
{
|
||||
var dict = new Dictionary<string, string>();
|
||||
|
||||
foreach (var (value, localeName) in strings)
|
||||
{
|
||||
dict[localeName] = value;
|
||||
}
|
||||
|
||||
return new LocalizedStringSet { Primary = strings[0].LocaleName, Values = dict };
|
||||
}
|
||||
|
||||
private sealed class Handle(SystemFontManagerDirectWrite parent, IDWriteFontFaceReference* fontFace) : BaseHandle(parent)
|
||||
{
|
||||
public readonly IDWriteFontFaceReference* FontFace = fontFace;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple implementation of a .NET Stream over a IDWriteFontFileStream.
|
||||
/// </summary>
|
||||
private sealed class DirectWriteStream : Stream
|
||||
{
|
||||
private readonly IDWriteFontFileStream* _stream;
|
||||
private readonly ulong _size;
|
||||
|
||||
private ulong _position;
|
||||
private bool _disposed;
|
||||
|
||||
public DirectWriteStream(IDWriteFontFileStream* stream)
|
||||
{
|
||||
_stream = stream;
|
||||
|
||||
fixed (ulong* pSize = &_size)
|
||||
{
|
||||
var result = _stream->GetFileSize(pSize);
|
||||
ThrowIfFailed(result, "IDWriteFontFileStream::GetFileSize");
|
||||
}
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return Read(buffer.AsSpan(offset, count));
|
||||
}
|
||||
|
||||
public override int Read(Span<byte> buffer)
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(DirectWriteStream));
|
||||
|
||||
var readLength = (uint)buffer.Length;
|
||||
if (readLength + _position > _size)
|
||||
readLength = (uint)(_size - _position);
|
||||
|
||||
void* fragmentStart;
|
||||
void* fragmentContext;
|
||||
|
||||
var result = _stream->ReadFileFragment(&fragmentStart, _position, readLength, &fragmentContext);
|
||||
ThrowIfFailed(result);
|
||||
|
||||
var data = new ReadOnlySpan<byte>(fragmentStart, (int)readLength);
|
||||
data.CopyTo(buffer);
|
||||
|
||||
_stream->ReleaseFileFragment(fragmentContext);
|
||||
|
||||
_position += readLength;
|
||||
return (int)readLength;
|
||||
}
|
||||
|
||||
public override long Seek(long offset, SeekOrigin origin)
|
||||
{
|
||||
switch (origin)
|
||||
{
|
||||
case SeekOrigin.Begin:
|
||||
Position = offset;
|
||||
break;
|
||||
case SeekOrigin.Current:
|
||||
Position += offset;
|
||||
break;
|
||||
case SeekOrigin.End:
|
||||
Position = Length + offset;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(origin), origin, null);
|
||||
}
|
||||
|
||||
return Position;
|
||||
}
|
||||
|
||||
public override void SetLength(long value)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
public override bool CanSeek => true;
|
||||
public override bool CanWrite => false;
|
||||
public override long Length => (long)_size;
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => (long)_position;
|
||||
set
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(value);
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThan((ulong)value, _size);
|
||||
|
||||
_position = (ulong)value;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_stream->Release();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
private record struct DWriteLocalizedString(string Value, string LocaleName);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Robust.Client.Graphics.FontManagement;
|
||||
|
||||
/// <summary>
|
||||
/// A fallback implementation of <see cref="ISystemFontManager"/> that just loads no fonts.
|
||||
/// </summary>
|
||||
internal sealed class SystemFontManagerFallback : ISystemFontManagerInternal
|
||||
{
|
||||
public void Initialize()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public bool IsSupported => false;
|
||||
public IEnumerable<ISystemFontFace> SystemFontFaces => [];
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
#if FREEDESKTOP
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Log;
|
||||
using SpaceWizards.Fontconfig.Interop;
|
||||
|
||||
namespace Robust.Client.Graphics.FontManagement;
|
||||
|
||||
internal sealed unsafe class SystemFontManagerFontconfig : SystemFontManagerBase, ISystemFontManagerInternal
|
||||
{
|
||||
private static readonly (int Fc, FontWidth Width)[] WidthTable = [
|
||||
(Fontconfig.FC_WIDTH_ULTRACONDENSED, FontWidth.UltraCondensed),
|
||||
(Fontconfig.FC_WIDTH_EXTRACONDENSED, FontWidth.ExtraCondensed),
|
||||
(Fontconfig.FC_WIDTH_CONDENSED, FontWidth.Condensed),
|
||||
(Fontconfig.FC_WIDTH_SEMICONDENSED, FontWidth.SemiCondensed),
|
||||
(Fontconfig.FC_WIDTH_NORMAL, FontWidth.Normal),
|
||||
(Fontconfig.FC_WIDTH_SEMIEXPANDED, FontWidth.SemiExpanded),
|
||||
(Fontconfig.FC_WIDTH_EXPANDED, FontWidth.Expanded),
|
||||
(Fontconfig.FC_WIDTH_EXTRAEXPANDED, FontWidth.ExtraExpanded),
|
||||
(Fontconfig.FC_WIDTH_ULTRAEXPANDED, FontWidth.UltraExpanded),
|
||||
];
|
||||
|
||||
public bool IsSupported => true;
|
||||
|
||||
public SystemFontManagerFontconfig(ILogManager logManager, IFontManagerInternal fontManager)
|
||||
: base(logManager, fontManager)
|
||||
{
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
Sawmill.Verbose("Initializing Fontconfig...");
|
||||
|
||||
var result = Fontconfig.FcInit();
|
||||
if (result == Fontconfig.FcFalse)
|
||||
throw new InvalidOperationException("Failed to initialize fontconfig!");
|
||||
|
||||
Sawmill.Verbose("Listing fonts...");
|
||||
|
||||
var os = Fontconfig.FcObjectSetCreate();
|
||||
AddToObjectSet(os, Fontconfig.FC_FAMILY);
|
||||
AddToObjectSet(os, Fontconfig.FC_FAMILYLANG);
|
||||
AddToObjectSet(os, Fontconfig.FC_STYLE);
|
||||
AddToObjectSet(os, Fontconfig.FC_STYLELANG);
|
||||
AddToObjectSet(os, Fontconfig.FC_FULLNAME);
|
||||
AddToObjectSet(os, Fontconfig.FC_FULLNAMELANG);
|
||||
AddToObjectSet(os, Fontconfig.FC_POSTSCRIPT_NAME);
|
||||
|
||||
AddToObjectSet(os, Fontconfig.FC_SLANT);
|
||||
AddToObjectSet(os, Fontconfig.FC_WEIGHT);
|
||||
AddToObjectSet(os, Fontconfig.FC_WIDTH);
|
||||
|
||||
AddToObjectSet(os, Fontconfig.FC_FILE);
|
||||
AddToObjectSet(os, Fontconfig.FC_INDEX);
|
||||
|
||||
var allPattern = Fontconfig.FcPatternCreate();
|
||||
var set = Fontconfig.FcFontList(null, allPattern, os);
|
||||
|
||||
for (var i = 0; i < set->nfont; i++)
|
||||
{
|
||||
var pattern = set->fonts[i];
|
||||
|
||||
try
|
||||
{
|
||||
LoadPattern(pattern);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Sawmill.Error($"Error while loading pattern: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Fontconfig.FcPatternDestroy(allPattern);
|
||||
Fontconfig.FcObjectSetDestroy(os);
|
||||
Fontconfig.FcFontSetDestroy(set);
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
// Nada.
|
||||
}
|
||||
|
||||
private void LoadPattern(FcPattern* pattern)
|
||||
{
|
||||
var path = PatternGetStrings(pattern, Fontconfig.FC_FILE)![0];
|
||||
var idx = PatternGetInts(pattern, Fontconfig.FC_INDEX)![0];
|
||||
|
||||
var family = PatternToLocalized(pattern, Fontconfig.FC_FAMILY, Fontconfig.FC_FAMILYLANG);
|
||||
var style = PatternToLocalized(pattern, Fontconfig.FC_STYLE, Fontconfig.FC_STYLELANG);
|
||||
var fullName = PatternToLocalized(pattern, Fontconfig.FC_FULLNAME, Fontconfig.FC_FULLNAMELANG);
|
||||
var psName = PatternGetStrings(pattern, Fontconfig.FC_POSTSCRIPT_NAME);
|
||||
if (psName == null)
|
||||
return;
|
||||
|
||||
var slant = PatternGetInts(pattern, Fontconfig.FC_SLANT) ?? [Fontconfig.FC_SLANT_ROMAN];
|
||||
var weight = PatternGetInts(pattern, Fontconfig.FC_WEIGHT) ?? [Fontconfig.FC_WEIGHT_REGULAR];
|
||||
var width = PatternGetInts(pattern, Fontconfig.FC_WIDTH) ?? [Fontconfig.FC_WIDTH_NORMAL];
|
||||
|
||||
Fonts.Add(new Handle(this)
|
||||
{
|
||||
FilePath = path,
|
||||
FileIndex = idx,
|
||||
FaceNames = style ?? LocalizedStringSet.Empty,
|
||||
FullNames = fullName ?? LocalizedStringSet.Empty,
|
||||
FamilyNames = family ?? LocalizedStringSet.Empty,
|
||||
PostscriptName = psName[0],
|
||||
Slant = SlantFromFontconfig(slant[0]),
|
||||
Weight = WeightFromFontconfig(weight[0]),
|
||||
Width = WidthFromFontconfig(width[0])
|
||||
});
|
||||
}
|
||||
|
||||
private static FontWeight WeightFromFontconfig(int value)
|
||||
{
|
||||
return (FontWeight)Fontconfig.FcWeightToOpenType(value);
|
||||
}
|
||||
|
||||
private static FontSlant SlantFromFontconfig(int value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
Fontconfig.FC_SLANT_ITALIC => FontSlant.Italic,
|
||||
Fontconfig.FC_SLANT_OBLIQUE => FontSlant.Italic,
|
||||
_ => FontSlant.Normal,
|
||||
};
|
||||
}
|
||||
|
||||
private static FontWidth WidthFromFontconfig(int value)
|
||||
{
|
||||
return WidthTable.MinBy(t => Math.Abs(t.Fc - value)).Width;
|
||||
}
|
||||
|
||||
private static unsafe void AddToObjectSet(FcObjectSet* os, ReadOnlySpan<byte> value)
|
||||
{
|
||||
var result = Fontconfig.FcObjectSetAdd(os, (sbyte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(value)));
|
||||
if (result == Fontconfig.FcFalse)
|
||||
throw new InvalidOperationException("Failed to add to object set!");
|
||||
}
|
||||
|
||||
private static unsafe string[]? PatternGetStrings(FcPattern* pattern, ReadOnlySpan<byte> @object)
|
||||
{
|
||||
return PatternGetValues(pattern, @object, static (FcPattern* p, sbyte* o, int i, out string value) =>
|
||||
{
|
||||
byte* str = null;
|
||||
var res = Fontconfig.FcPatternGetString(p, o, i, &str);
|
||||
value = Marshal.PtrToStringUTF8((nint)str)!;
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
private static unsafe int[]? PatternGetInts(FcPattern* pattern, ReadOnlySpan<byte> @object)
|
||||
{
|
||||
return PatternGetValues(pattern, @object, static (FcPattern* p, sbyte* o, int i, out int value) =>
|
||||
{
|
||||
FcResult res;
|
||||
fixed (int* pValue = &value)
|
||||
{
|
||||
res = Fontconfig.FcPatternGetInteger(p, o, i, pValue);
|
||||
}
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
private delegate FcResult GetValue<T>(FcPattern* p, sbyte* o, int i, out T value);
|
||||
private static unsafe T[]? PatternGetValues<T>(FcPattern* pattern, ReadOnlySpan<byte> @object, GetValue<T> getValue)
|
||||
{
|
||||
var list = new List<T>();
|
||||
|
||||
var i = 0;
|
||||
while (true)
|
||||
{
|
||||
var result = getValue(pattern, (sbyte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(@object)), i++, out var value);
|
||||
if (result == FcResult.FcResultMatch)
|
||||
{
|
||||
list.Add(value);
|
||||
}
|
||||
else if (result == FcResult.FcResultNoMatch)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else if (result == FcResult.FcResultNoId)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"FcPatternGetString gave error: {result}");
|
||||
}
|
||||
}
|
||||
|
||||
return list.ToArray();
|
||||
}
|
||||
|
||||
private static LocalizedStringSet? PatternToLocalized(FcPattern* pattern, ReadOnlySpan<byte> @object, ReadOnlySpan<byte> objectLang)
|
||||
{
|
||||
var values = PatternGetStrings(pattern, @object);
|
||||
var languages = PatternGetStrings(pattern, objectLang);
|
||||
|
||||
if (values == null || languages == null || values.Length == 0 || languages.Length != values.Length)
|
||||
return null;
|
||||
|
||||
var dict = new Dictionary<string, string>();
|
||||
|
||||
for (var i = 0; i < values.Length; i++)
|
||||
{
|
||||
var val = values[i];
|
||||
var lang = languages[i];
|
||||
|
||||
dict.TryAdd(lang, val);
|
||||
}
|
||||
|
||||
return new LocalizedStringSet
|
||||
{
|
||||
Primary = languages[0],
|
||||
Values = dict
|
||||
};
|
||||
}
|
||||
|
||||
protected override IFontFaceHandle LoadFontFace(BaseHandle handle)
|
||||
{
|
||||
var cast = (Handle)handle;
|
||||
|
||||
return FontManager.Load(new MemoryMappedFontMemoryHandle(cast.FilePath), cast.FileIndex);
|
||||
}
|
||||
|
||||
private sealed class Handle(SystemFontManagerFontconfig parent) : BaseHandle(parent)
|
||||
{
|
||||
public required string FilePath;
|
||||
public required int FileIndex;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1,16 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using SharpFont;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
@@ -20,6 +20,7 @@ namespace Robust.Client.Graphics
|
||||
private const int SheetHeight = 256;
|
||||
|
||||
private readonly IClyde _clyde;
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
private uint _baseFontDpi = 96;
|
||||
|
||||
@@ -28,22 +29,56 @@ namespace Robust.Client.Graphics
|
||||
private readonly Dictionary<(FontFaceHandle, int fontSize), FontInstanceHandle> _loadedInstances =
|
||||
new();
|
||||
|
||||
public FontManager(IClyde clyde)
|
||||
public FontManager(IClyde clyde, ILogManager logManager)
|
||||
{
|
||||
_clyde = clyde;
|
||||
_library = new Library();
|
||||
_sawmill = logManager.GetSawmill("font");
|
||||
}
|
||||
|
||||
public IFontFaceHandle Load(Stream stream)
|
||||
public IFontFaceHandle Load(Stream stream, int index = 0)
|
||||
{
|
||||
// Freetype directly operates on the font memory managed by us.
|
||||
// As such, the font data should be pinned in POH.
|
||||
var fontData = stream.CopyToPinnedArray();
|
||||
var face = new Face(_library, fontData, 0);
|
||||
var handle = new FontFaceHandle(face);
|
||||
return Load(new ArrayMemoryHandle(fontData), index);
|
||||
}
|
||||
|
||||
public IFontFaceHandle Load(IFontMemoryHandle memory, int index = 0)
|
||||
{
|
||||
var face = FaceLoad(memory, index);
|
||||
var handle = new FontFaceHandle(face, memory);
|
||||
return handle;
|
||||
}
|
||||
|
||||
public IFontFaceHandle LoadWithPostscriptName(IFontMemoryHandle memory, string postscriptName)
|
||||
{
|
||||
var numFaces = 1;
|
||||
|
||||
for (var i = 0; i < numFaces; i++)
|
||||
{
|
||||
var face = FaceLoad(memory, i);
|
||||
numFaces = face.FaceCount;
|
||||
|
||||
if (face.GetPostscriptName() == postscriptName)
|
||||
return new FontFaceHandle(face, memory);
|
||||
|
||||
face.Dispose();
|
||||
}
|
||||
|
||||
// Fallback, load SOMETHING.
|
||||
_sawmill.Warning($"Failed to load correct font via postscript name! {postscriptName}");
|
||||
return new FontFaceHandle(FaceLoad(memory, 0), memory);
|
||||
}
|
||||
|
||||
private unsafe Face FaceLoad(IFontMemoryHandle memory, int index)
|
||||
{
|
||||
return new Face(_library,
|
||||
(nint)memory.GetData(),
|
||||
checked((int)memory.GetDataSize()),
|
||||
index);
|
||||
}
|
||||
|
||||
void IFontManagerInternal.SetFontDpi(uint fontDpi)
|
||||
{
|
||||
_baseFontDpi = fontDpi;
|
||||
@@ -235,10 +270,13 @@ namespace Robust.Client.Graphics
|
||||
|
||||
private sealed class FontFaceHandle : IFontFaceHandle
|
||||
{
|
||||
// Keep this alive to avoid it being GC'd.
|
||||
private readonly IFontMemoryHandle _memoryHandle;
|
||||
public Face Face { get; }
|
||||
|
||||
public FontFaceHandle(Face face)
|
||||
public FontFaceHandle(Face face, IFontMemoryHandle memoryHandle)
|
||||
{
|
||||
_memoryHandle = memoryHandle;
|
||||
Face = face;
|
||||
}
|
||||
}
|
||||
@@ -377,5 +415,32 @@ namespace Robust.Client.Graphics
|
||||
public CharMetrics Metrics;
|
||||
public AtlasTexture? Texture;
|
||||
}
|
||||
|
||||
private sealed class ArrayMemoryHandle(byte[] array) : IFontMemoryHandle
|
||||
{
|
||||
private GCHandle _gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned);
|
||||
|
||||
public unsafe byte* GetData()
|
||||
{
|
||||
return (byte*) _gcHandle.AddrOfPinnedObject();
|
||||
}
|
||||
|
||||
public IntPtr GetDataSize()
|
||||
{
|
||||
return array.Length;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_gcHandle.Free();
|
||||
_gcHandle = default;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~ArrayMemoryHandle()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Robust.Shared.Graphics;
|
||||
|
||||
namespace Robust.Client.Graphics
|
||||
{
|
||||
@@ -10,7 +10,15 @@ namespace Robust.Client.Graphics
|
||||
}
|
||||
internal interface IFontManagerInternal : IFontManager
|
||||
{
|
||||
IFontFaceHandle Load(Stream stream);
|
||||
IFontFaceHandle Load(Stream stream, int index = 0);
|
||||
IFontFaceHandle Load(IFontMemoryHandle memory, int index = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Load a specified font in a font collection.
|
||||
/// </summary>
|
||||
/// <param name="memory">Memory for the entire font collection.</param>
|
||||
/// <param name="postscriptName">The postscript name of the font to load.</param>
|
||||
IFontFaceHandle LoadWithPostscriptName(IFontMemoryHandle memory, string postscriptName);
|
||||
IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size);
|
||||
void SetFontDpi(uint fontDpi);
|
||||
}
|
||||
@@ -22,8 +30,6 @@ namespace Robust.Client.Graphics
|
||||
|
||||
internal interface IFontInstanceHandle
|
||||
{
|
||||
|
||||
|
||||
Texture? GetCharTexture(Rune codePoint, float scale);
|
||||
Texture? GetCharTexture(char chr, float scale) => GetCharTexture((Rune) chr, scale);
|
||||
CharMetrics? GetCharMetrics(Rune codePoint, float scale);
|
||||
@@ -35,6 +41,12 @@ namespace Robust.Client.Graphics
|
||||
int GetLineHeight(float scale);
|
||||
}
|
||||
|
||||
internal unsafe interface IFontMemoryHandle : IDisposable
|
||||
{
|
||||
byte* GetData();
|
||||
nint GetDataSize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Metrics for a single glyph in a font.
|
||||
/// Refer to https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html for more information.
|
||||
|
||||
127
Robust.Client/Graphics/ISystemFontManager.cs
Normal file
127
Robust.Client/Graphics/ISystemFontManager.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Robust.Client.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Provides access to fonts installed on the user's operating system.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Different operating systems ship different fonts, so you should generally not rely on any one
|
||||
/// specific font being available. This system is primarily provided for allowing user preference.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="ISystemFontFace"/>
|
||||
public interface ISystemFontManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether access to system fonts is currently supported on this platform.
|
||||
/// </summary>
|
||||
bool IsSupported { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The list of font face available from the operating system.
|
||||
/// </summary>
|
||||
IEnumerable<ISystemFontFace> SystemFontFaces { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single font face, provided by the user's operating system.
|
||||
/// </summary>
|
||||
/// <seealso cref="ISystemFontManager"/>
|
||||
public interface ISystemFontFace
|
||||
{
|
||||
/// <summary>
|
||||
/// The PostScript name of the font face.
|
||||
/// This is generally the closest to an unambiguous unique identifier as you're going to get.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For example, "Arial-ItalicMT"
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
string PostscriptName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The full name of the font face, localized to the current locale.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For example, "Arial Cursiva"
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="GetLocalizedFullName"/>
|
||||
string FullName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The family name of the font face, localized to the current locale.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For example, "Arial"
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="GetLocalizedFamilyName"/>
|
||||
string FamilyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The face name (or "style name") of the font face, localized to the current locale.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// For example, "Cursiva"
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <seealso cref="GetLocalizedFaceName"/>
|
||||
string FaceName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="FullName"/>, localized to a specific locale.
|
||||
/// </summary>
|
||||
/// <param name="culture">The locale to fetch the localized string for.</param>
|
||||
string GetLocalizedFullName(CultureInfo culture);
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="FamilyName"/>, localized to a specific locale.
|
||||
/// </summary>
|
||||
/// <param name="culture">The locale to fetch the localized string for.</param>
|
||||
string GetLocalizedFamilyName(CultureInfo culture);
|
||||
|
||||
/// <summary>
|
||||
/// Get the <see cref="FaceName"/>, localized to a specific locale.
|
||||
/// </summary>
|
||||
/// <param name="culture">The locale to fetch the localized string for.</param>
|
||||
string GetLocalizedFaceName(CultureInfo culture);
|
||||
|
||||
/// <summary>
|
||||
/// The weight of the font face.
|
||||
/// </summary>
|
||||
FontWeight Weight { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The slant of the font face.
|
||||
/// </summary>
|
||||
FontSlant Slant { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The width of the font face.
|
||||
/// </summary>
|
||||
FontWidth Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Load the font face so that it can be used in-engine.
|
||||
/// </summary>
|
||||
/// <param name="size">The size to load the font at.</param>
|
||||
/// <returns>A font object that can be used to render text.</returns>
|
||||
Font Load(int size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Engine-internal API for <see cref="ISystemFontManager"/>.
|
||||
/// </summary>
|
||||
internal interface ISystemFontManagerInternal : ISystemFontManager
|
||||
{
|
||||
void Initialize();
|
||||
void Shutdown();
|
||||
}
|
||||
306
Robust.Client/Graphics/LoadingScreenManager.cs
Normal file
306
Robust.Client/Graphics/LoadingScreenManager.cs
Normal file
@@ -0,0 +1,306 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using Stopwatch = Robust.Shared.Timing.Stopwatch;
|
||||
|
||||
namespace Robust.Client.Graphics;
|
||||
|
||||
internal interface ILoadingScreenManager
|
||||
{
|
||||
void BeginLoadingSection(string sectionName);
|
||||
|
||||
/// <summary>
|
||||
/// Start a loading bar "section" for the given method.
|
||||
/// Must be ended with EndSection.
|
||||
/// </summary>
|
||||
void BeginLoadingSection(object method);
|
||||
|
||||
void EndLoadingSection();
|
||||
|
||||
/// <summary>
|
||||
/// Will run the giving function and add a custom "section" for it on the loading screen.
|
||||
/// </summary>
|
||||
void LoadingStep(Action action, object method);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manager that creates and displays a basic splash screen and loading bar.
|
||||
/// </summary>
|
||||
internal sealed class LoadingScreenManager : ILoadingScreenManager
|
||||
{
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IClydeInternal _clyde = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private readonly Stopwatch _sw = new();
|
||||
|
||||
#region UI constants
|
||||
|
||||
private const int LoadingBarWidth = 250;
|
||||
private const int LoadingBarHeight = 20;
|
||||
private const int LoadingBarOutlineOffset = 5;
|
||||
private static readonly Vector2i LogoLoadingBarOffset = (0, 20);
|
||||
private static readonly Vector2i LoadTimesIndent = (20, 0);
|
||||
|
||||
private const int NumLongestLoadTimes = 5;
|
||||
|
||||
private static readonly Color LoadingBarColor = Color.White;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cvars
|
||||
|
||||
private string _splashLogo = "";
|
||||
private bool _showLoadingBar;
|
||||
private bool _showDebug;
|
||||
|
||||
#endregion
|
||||
|
||||
private const string FontLocation = "/EngineFonts/NotoSans/NotoSans-Regular.ttf";
|
||||
private const int FontSize = 11;
|
||||
private VectorFont? _font;
|
||||
|
||||
// Number of loading sections for the loading bar. This has to be manually set!
|
||||
private int _numberOfLoadingSections;
|
||||
|
||||
// The name of the section and how much time it took to load
|
||||
internal readonly List<(string Name, TimeSpan LoadTime)> Times = [];
|
||||
|
||||
private int _currentSection;
|
||||
private string? _currentSectionName;
|
||||
|
||||
private bool _currentlyInSection;
|
||||
private bool _finished;
|
||||
|
||||
public void Initialize(int sections)
|
||||
{
|
||||
if (_finished)
|
||||
return;
|
||||
|
||||
_clyde.VsyncEnabled = false;
|
||||
|
||||
_numberOfLoadingSections = sections;
|
||||
|
||||
_sawmill = _logManager.GetSawmill("loading");
|
||||
|
||||
_splashLogo = _cfg.GetCVar(CVars.DisplaySplashLogo);
|
||||
_showLoadingBar = _cfg.GetCVar(CVars.LoadingShowBar);
|
||||
_showDebug = _cfg.GetCVar(CVars.LoadingShowDebug);
|
||||
|
||||
if (_resourceCache.TryGetResource<FontResource>(FontLocation, out var fontResource))
|
||||
_font = new VectorFont(fontResource, FontSize);
|
||||
else
|
||||
_sawmill.Error($"Could not load font: {FontLocation}");
|
||||
}
|
||||
|
||||
public void BeginLoadingSection(string sectionName) => BeginLoadingSection(sectionName, false);
|
||||
public void BeginLoadingSection(string sectionName, bool dontRender)
|
||||
{
|
||||
if (_finished)
|
||||
return;
|
||||
|
||||
if (_currentlyInSection)
|
||||
throw new InvalidOperationException("You cannot begin more than one section at a time!");
|
||||
|
||||
_currentlyInSection = true;
|
||||
|
||||
_currentSectionName = sectionName;
|
||||
|
||||
if (!dontRender)
|
||||
{
|
||||
// This ensures that if the screen was resized or something the new size is properly updated to clyde.
|
||||
_clyde.ProcessInput(new FrameEventArgs((float)_sw.Elapsed.TotalSeconds));
|
||||
_sw.Restart();
|
||||
_clyde.Render();
|
||||
}
|
||||
else
|
||||
{
|
||||
_sw.Restart();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start a loading bar "section" for the given method.
|
||||
/// Must be ended with EndSection.
|
||||
/// </summary>
|
||||
public void BeginLoadingSection(object method)
|
||||
{
|
||||
if (_finished)
|
||||
return;
|
||||
|
||||
BeginLoadingSection(method.GetType().Name);
|
||||
}
|
||||
|
||||
public void EndLoadingSection()
|
||||
{
|
||||
if (_finished)
|
||||
return;
|
||||
|
||||
var time = _sw.Elapsed;
|
||||
if (_currentSectionName != null)
|
||||
Times.Add((_currentSectionName, time));
|
||||
_currentSection++;
|
||||
_currentlyInSection = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will run the giving function and add a custom "section" for it on the loading screen.
|
||||
/// </summary>
|
||||
public void LoadingStep(Action action, object method)
|
||||
{
|
||||
if (_finished)
|
||||
return;
|
||||
|
||||
BeginLoadingSection(method as string ?? method.GetType().Name);
|
||||
action();
|
||||
EndLoadingSection();
|
||||
}
|
||||
|
||||
public void Finish()
|
||||
{
|
||||
if (_finished)
|
||||
return;
|
||||
|
||||
if (_currentSection != _numberOfLoadingSections)
|
||||
_sawmill.Error($"The number of seen loading sections isn't equal to the total number of loading sections! Seen: {_currentSection}, Total: {_numberOfLoadingSections}");
|
||||
|
||||
_finished = true;
|
||||
}
|
||||
|
||||
#region Drawing functions
|
||||
|
||||
/// <summary>
|
||||
/// Draw out the splash and loading screen.
|
||||
/// </summary>
|
||||
public void DrawLoadingScreen(IRenderHandle handle, Vector2i screenSize)
|
||||
{
|
||||
if (_finished)
|
||||
return;
|
||||
|
||||
var scale = UserInterfaceManager.CalculateUIScale(_clyde.MainWindow.ContentScale.X, _cfg);
|
||||
|
||||
// Start at the center!
|
||||
var location = screenSize / 2;
|
||||
|
||||
DrawSplash(handle, ref location, scale);
|
||||
|
||||
DrawLoadingBar(handle, ref location, scale);
|
||||
|
||||
if (_showDebug)
|
||||
{
|
||||
DrawCurrentLoading(handle, ref location, scale);
|
||||
|
||||
DrawTopTimes(handle, ref location, scale);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawSplash(IRenderHandle handle, ref Vector2i startLocation, float scale)
|
||||
{
|
||||
if (!_resourceCache.TryGetResource<TextureResource>(_splashLogo, out var textureResource))
|
||||
return;
|
||||
|
||||
var drawSize = textureResource.Texture.Size * scale;
|
||||
|
||||
handle.DrawingHandleScreen.DrawTextureRect(textureResource.Texture, UIBox2.FromDimensions(startLocation - drawSize / 2, drawSize));
|
||||
startLocation += Vector2i.Up * (int) drawSize.Y / 2;
|
||||
}
|
||||
|
||||
private void DrawLoadingBar(IRenderHandle handle, ref Vector2i location, float scale)
|
||||
{
|
||||
var barWidth = (int)(LoadingBarWidth * scale);
|
||||
var barHeight = (int)(LoadingBarHeight * scale);
|
||||
var outlineOffset = (int)(LoadingBarOutlineOffset * scale);
|
||||
|
||||
// Always do the offsets, it looks a lot better!
|
||||
location.X -= barWidth / 2;
|
||||
location += (Vector2i) (LogoLoadingBarOffset * scale);
|
||||
|
||||
if (!_showLoadingBar)
|
||||
return;
|
||||
|
||||
var sectionWidth = barWidth / _numberOfLoadingSections;
|
||||
|
||||
var barTopLeft = location;
|
||||
var barBottomRight = new Vector2i(_currentSection * sectionWidth % barWidth, barHeight);
|
||||
var barBottomRightMax = new Vector2i(barWidth, barHeight);
|
||||
|
||||
var outlinePosition = barTopLeft + Vector2i.DownLeft * outlineOffset;
|
||||
var outlineSize = barBottomRightMax + Vector2i.UpRight * 2 * outlineOffset;
|
||||
|
||||
// Outline
|
||||
handle.DrawingHandleScreen.DrawRect(UIBox2.FromDimensions(outlinePosition, outlineSize), LoadingBarColor, false);
|
||||
|
||||
// Progress bar
|
||||
handle.DrawingHandleScreen.DrawRect(UIBox2.FromDimensions(barTopLeft, barBottomRight), LoadingBarColor);
|
||||
|
||||
location += Vector2i.Up * outlineSize;
|
||||
}
|
||||
|
||||
// Draw the currently loading section to the screen.
|
||||
private void DrawCurrentLoading(IRenderHandle handle, ref Vector2i location, float scale)
|
||||
{
|
||||
if (_font == null || _currentSectionName == null)
|
||||
return;
|
||||
|
||||
handle.DrawingHandleScreen.DrawString(_font, location, _currentSectionName, scale, Color.White);
|
||||
location += Vector2i.Up * _font.GetLineHeight(scale);
|
||||
}
|
||||
|
||||
// Draw the slowest loading times to the screen.
|
||||
private void DrawTopTimes(IRenderHandle handle, ref Vector2i location, float scale)
|
||||
{
|
||||
if (_font == null)
|
||||
return;
|
||||
|
||||
location += (Vector2i)(LoadTimesIndent * scale);
|
||||
|
||||
var offset = 0;
|
||||
var x = 0;
|
||||
Times.Sort((a, b) => b.LoadTime.CompareTo(a.LoadTime));
|
||||
|
||||
foreach (var (name, time) in Times)
|
||||
{
|
||||
if (x >= NumLongestLoadTimes)
|
||||
break;
|
||||
|
||||
var entry = $"{time.TotalSeconds:F2} - {name}";
|
||||
handle.DrawingHandleScreen.DrawString(_font, location + new Vector2i(0, offset), entry, scale, Color.White);
|
||||
offset += _font.GetLineHeight(scale);
|
||||
x++;
|
||||
}
|
||||
|
||||
location += Vector2i.Up * offset;
|
||||
}
|
||||
|
||||
#endregion // Drawing functions
|
||||
}
|
||||
|
||||
internal sealed class ShowTopLoadingTimesCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly LoadingScreenManager _mgr = default!;
|
||||
|
||||
public string Command => "loading_top";
|
||||
public string Description => "";
|
||||
public string Help => "";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var sorted = _mgr.Times.Where(x => x.LoadTime > TimeSpan.FromSeconds(0.01)).OrderByDescending(x => x.LoadTime);
|
||||
foreach (var (name, time) in sorted)
|
||||
{
|
||||
shell.WriteLine($"{time.TotalSeconds:F2} - {name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
89
Robust.Client/Graphics/SystemFontManager.cs
Normal file
89
Robust.Client/Graphics/SystemFontManager.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics.FontManagement;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Robust.Client.Graphics;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="ISystemFontManager"/> that proxies to platform-specific implementations,
|
||||
/// and adds additional logging.
|
||||
/// </summary>
|
||||
internal sealed class SystemFontManager : ISystemFontManagerInternal, IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IFontManagerInternal _fontManager = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private ISystemFontManagerInternal _implementation = default!;
|
||||
|
||||
public bool IsSupported => _implementation.IsSupported;
|
||||
public IEnumerable<ISystemFontFace> SystemFontFaces => _implementation.SystemFontFaces;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_implementation = GetImplementation();
|
||||
_sawmill.Verbose($"Using {_implementation.GetType()}");
|
||||
|
||||
_sawmill.Debug("Initializing system font manager implementation");
|
||||
try
|
||||
{
|
||||
var sw = RStopwatch.StartNew();
|
||||
_implementation.Initialize();
|
||||
_sawmill.Debug($"Done initializing system font manager in {sw.Elapsed}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// This is a non-critical engine system that has to parse significant amounts of external data.
|
||||
// Best to fail gracefully to avoid full startup failures.
|
||||
|
||||
_sawmill.Error($"Error while initializing system font manager, resorting to fallback: {e}");
|
||||
_implementation = new SystemFontManagerFallback();
|
||||
}
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_sawmill.Verbose("Shutting down system font manager");
|
||||
|
||||
try
|
||||
{
|
||||
_implementation.Shutdown();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Exception shutting down system font manager: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
_sawmill.Verbose("Successfully shut down system font manager");
|
||||
}
|
||||
|
||||
private ISystemFontManagerInternal GetImplementation()
|
||||
{
|
||||
if (!_cfg.GetCVar(CVars.FontSystem))
|
||||
return new SystemFontManagerFallback();
|
||||
|
||||
#if WINDOWS
|
||||
return new SystemFontManagerDirectWrite(_logManager, _cfg, _fontManager);
|
||||
#elif FREEDESKTOP
|
||||
return new SystemFontManagerFontconfig(_logManager, _fontManager);
|
||||
#elif MACOS
|
||||
return new SystemFontManagerCoreText(_logManager, _fontManager);
|
||||
#else
|
||||
return new SystemFontManagerFallback();
|
||||
#endif
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("font.system");
|
||||
// _sawmill.Level = LogLevel.Verbose;
|
||||
}
|
||||
}
|
||||
24
Robust.Client/Interop/MacOS/AppKit.cs
Normal file
24
Robust.Client/Interop/MacOS/AppKit.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// ReSharper disable InconsistentNaming
|
||||
#if MACOS
|
||||
|
||||
namespace Robust.Client.Interop.MacOS;
|
||||
|
||||
/// <summary>
|
||||
/// Binding to macOS AppKit.
|
||||
/// </summary>
|
||||
internal static class AppKit
|
||||
{
|
||||
// Values pulled from here:
|
||||
// https://chromium.googlesource.com/chromium/src/+/b5019b491932dfa597acb3a13a9e7780fb6525a9/ui/gfx/platform_font_mac.mm#53
|
||||
public const double NSFontWeightUltraLight = -0.8;
|
||||
public const double NSFontWeightThin = -0.6;
|
||||
public const double NSFontWeightLight = -0.4;
|
||||
public const double NSFontWeightRegular = 0;
|
||||
public const double NSFontWeightMedium = 0.23;
|
||||
public const double NSFontWeightSemiBold = 0.30;
|
||||
public const double NSFontWeightBold = 0.40;
|
||||
public const double NSFontWeightHeavy = 0.56;
|
||||
public const double NSFontWeightBlack = 0.62;
|
||||
}
|
||||
|
||||
#endif
|
||||
97
Robust.Client/Interop/MacOS/CoreFoundation.cs
Normal file
97
Robust.Client/Interop/MacOS/CoreFoundation.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
#if MACOS
|
||||
using System.Runtime.InteropServices;
|
||||
using CFIndex = System.Runtime.InteropServices.CLong;
|
||||
using Boolean = byte;
|
||||
|
||||
namespace Robust.Client.Interop.MacOS;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
/// <summary>
|
||||
/// Binding to macOS Core Foundation.
|
||||
/// </summary>
|
||||
internal static unsafe class CoreFoundation
|
||||
{
|
||||
private const string CoreFoundationLibrary = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
|
||||
|
||||
public const int kCFNumberFloat32Type = 5;
|
||||
|
||||
public static string CFStringToManaged(__CFString* str)
|
||||
{
|
||||
var length = CFStringGetLength(str);
|
||||
|
||||
return string.Create(
|
||||
checked((int)length.Value),
|
||||
(nint)str,
|
||||
static (span, arg) =>
|
||||
{
|
||||
fixed (char* pBuffer = span)
|
||||
{
|
||||
CFStringGetCharacters((__CFString*)arg,
|
||||
new CFRange
|
||||
{
|
||||
location = new CFIndex(0),
|
||||
length = new CFIndex(span.Length),
|
||||
},
|
||||
pBuffer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern void* CFRetain(void* cf);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern void CFRelease(void* cf);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern CFIndex CFArrayGetCount(__CFArray* array);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern void* CFArrayGetValueAtIndex(__CFArray* array, CFIndex index);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern CFIndex CFStringGetLength(__CFString* str);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern void CFStringGetCharacters(__CFString* str, CFRange range, char* buffer);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern Boolean CFURLGetFileSystemRepresentation(
|
||||
__CFURL* url,
|
||||
Boolean resolveAgainstBase,
|
||||
byte* buffer,
|
||||
CFIndex maxBufLen);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern __CFString* CFURLGetString(__CFURL* url);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern CFIndex CFDictionaryGetCount(__CFDictionary* theDict);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern void* CFDictionaryGetValue(__CFDictionary* theDict, void* key);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern void CFDictionaryGetKeysAndValues(__CFDictionary* theDict, void** keys, void** values);
|
||||
|
||||
[DllImport(CoreFoundationLibrary)]
|
||||
internal static extern void CFNumberGetValue(__CFNumber* number, CLong theType, void* valuePtr);
|
||||
}
|
||||
|
||||
internal struct __CFNumber;
|
||||
|
||||
internal struct __CFString;
|
||||
|
||||
internal struct __CFURL;
|
||||
|
||||
internal struct __CFArray;
|
||||
|
||||
internal struct __CFDictionary;
|
||||
|
||||
internal struct CFRange
|
||||
{
|
||||
public CFIndex location;
|
||||
public CFIndex length;
|
||||
}
|
||||
#endif
|
||||
54
Robust.Client/Interop/MacOS/CoreText.cs
Normal file
54
Robust.Client/Interop/MacOS/CoreText.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
#if MACOS
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Robust.Client.Interop.MacOS;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
/// <summary>
|
||||
/// Binding to macOS Core Text.
|
||||
/// </summary>
|
||||
internal static unsafe class CoreText
|
||||
{
|
||||
private const string CoreTextLibrary = "/System/Library/Frameworks/CoreText.framework/CoreText";
|
||||
|
||||
public static readonly __CFString* kCTFontURLAttribute;
|
||||
public static readonly __CFString* kCTFontNameAttribute;
|
||||
public static readonly __CFString* kCTFontDisplayNameAttribute;
|
||||
public static readonly __CFString* kCTFontFamilyNameAttribute;
|
||||
public static readonly __CFString* kCTFontStyleNameAttribute;
|
||||
public static readonly __CFString* kCTFontTraitsAttribute;
|
||||
public static readonly __CFString* kCTFontWeightTrait;
|
||||
public static readonly __CFString* kCTFontWidthTrait;
|
||||
public static readonly __CFString* kCTFontSlantTrait;
|
||||
|
||||
static CoreText()
|
||||
{
|
||||
var lib = NativeLibrary.Load(CoreTextLibrary);
|
||||
kCTFontURLAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontURLAttribute));
|
||||
kCTFontNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontNameAttribute));
|
||||
kCTFontDisplayNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontDisplayNameAttribute));
|
||||
kCTFontFamilyNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontFamilyNameAttribute));
|
||||
kCTFontStyleNameAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontStyleNameAttribute));
|
||||
kCTFontTraitsAttribute = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontTraitsAttribute));
|
||||
kCTFontWeightTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontWeightTrait));
|
||||
kCTFontWidthTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontWidthTrait));
|
||||
kCTFontSlantTrait = *(__CFString**)NativeLibrary.GetExport(lib, nameof(kCTFontSlantTrait));
|
||||
}
|
||||
|
||||
[DllImport(CoreTextLibrary)]
|
||||
public static extern __CTFontCollection* CTFontCollectionCreateFromAvailableFonts(__CFDictionary* options);
|
||||
|
||||
[DllImport(CoreTextLibrary)]
|
||||
public static extern __CFArray* CTFontCollectionCreateMatchingFontDescriptors(__CTFontCollection* collection);
|
||||
|
||||
[DllImport(CoreTextLibrary)]
|
||||
public static extern void* CTFontDescriptorCopyAttribute(__CTFontDescriptor* descriptor, __CFString* attribute);
|
||||
|
||||
[DllImport(CoreTextLibrary)]
|
||||
public static extern __CFDictionary* CTFontDescriptorCopyAttributes(__CTFontDescriptor* descriptor);
|
||||
}
|
||||
|
||||
internal struct __CTFontCollection;
|
||||
internal struct __CTFontDescriptor;
|
||||
#endif
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -105,6 +106,7 @@ public sealed partial class PhysicsSystem
|
||||
}
|
||||
|
||||
UpdateIsTouching(contacts);
|
||||
DispatchEvents();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -141,7 +143,8 @@ public sealed partial class PhysicsSystem
|
||||
if ((contact.Flags & ContactFlags.Filter) != 0x0)
|
||||
{
|
||||
if (!ShouldCollide(fixtureA, fixtureB) ||
|
||||
!ShouldCollide(uidA, uidB, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB))
|
||||
!ShouldCollideSlow(uidA, uidB, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB) ||
|
||||
!ShouldCollideJoints(uidA, uidB))
|
||||
{
|
||||
contact.IsTouching = false;
|
||||
continue;
|
||||
@@ -206,7 +209,20 @@ public sealed partial class PhysicsSystem
|
||||
var uidB = contact.EntityB;
|
||||
var bodyATransform = GetPhysicsTransform(uidA, xformQuery.GetComponent(uidA));
|
||||
var bodyBTransform = GetPhysicsTransform(uidB, xformQuery.GetComponent(uidB));
|
||||
var wasTouching = contact.IsTouching;
|
||||
|
||||
contact.UpdateIsTouching(bodyATransform, bodyBTransform);
|
||||
var points = new FixedArray4<Vector2>();
|
||||
|
||||
// Need to re-run the event otherwise we skip the StartCollideEvent running again later in the physics step.
|
||||
if (!wasTouching && contact.IsTouching)
|
||||
{
|
||||
contact.GetWorldManifold(bodyATransform, bodyBTransform, out var worldNormal, points.AsSpan);
|
||||
// Use the 3rd Vector2 as the world normal, 4th is blank.
|
||||
points._02 = worldNormal;
|
||||
}
|
||||
|
||||
RunContactEvents(Contact.GetContactStatus(contact, wasTouching), contact, points);
|
||||
}
|
||||
|
||||
ArrayPool<Contact>.Shared.Return(contacts);
|
||||
|
||||
@@ -7,7 +7,6 @@ using System.Runtime.CompilerServices;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.ResourceManagement;
|
||||
@@ -17,6 +16,7 @@ namespace Robust.Client.ResourceManagement;
|
||||
/// </summary>
|
||||
internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInternal, IDisposable
|
||||
{
|
||||
[Shared.IoC.Dependency] private readonly IDependencyCollection _deps = default!;
|
||||
private readonly Dictionary<Type, TypeData> _cachedResources = new();
|
||||
private readonly Dictionary<Type, BaseResource> _fallbacks = new();
|
||||
|
||||
@@ -36,8 +36,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
|
||||
var resource = new T();
|
||||
try
|
||||
{
|
||||
var dependencies = IoCManager.Instance!;
|
||||
resource.Load(dependencies, path);
|
||||
resource.Load(_deps, path);
|
||||
cache.Resources[path] = resource;
|
||||
return resource;
|
||||
}
|
||||
@@ -81,8 +80,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
|
||||
var _resource = new T();
|
||||
try
|
||||
{
|
||||
var dependencies = IoCManager.Instance!;
|
||||
_resource.Load(dependencies, path);
|
||||
_resource.Load(_deps, path);
|
||||
resource = _resource;
|
||||
cache.Resources[path] = resource;
|
||||
return true;
|
||||
@@ -123,8 +121,7 @@ internal sealed partial class ResourceCache : ResourceManager, IResourceCacheInt
|
||||
|
||||
try
|
||||
{
|
||||
var dependencies = IoCManager.Instance!;
|
||||
res.Reload(dependencies, path);
|
||||
res.Reload(_deps, path);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -70,6 +70,11 @@
|
||||
|
||||
<Import Project="..\MSBuild\Robust.Properties.targets" />
|
||||
|
||||
<ItemGroup Condition="'$(IsFreedesktop)' == 'True'">
|
||||
<PackageReference Include="SpaceWizards.Fontconfig.Interop" />
|
||||
<RobustLinkAssemblies Include="SpaceWizards.Fontconfig.Interop" />
|
||||
</ItemGroup>
|
||||
|
||||
<Import Project="..\MSBuild\XamlIL.targets" />
|
||||
|
||||
<Import Project="..\MSBuild\Robust.Trimming.targets" />
|
||||
|
||||
@@ -19,6 +19,9 @@ namespace Robust.Client.UserInterface
|
||||
get => _stylesheet;
|
||||
set
|
||||
{
|
||||
if (ReferenceEquals(_stylesheet, value))
|
||||
return;
|
||||
|
||||
_stylesheet = value;
|
||||
StylesheetUpdateRecursive();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using Robust.Client.UserInterface.Themes;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -74,6 +75,8 @@ namespace Robust.Client.UserInterface
|
||||
// _nameScope = nameScope;
|
||||
//}
|
||||
|
||||
public virtual ISawmill Log => UserInterfaceManager.ControlSawmill;
|
||||
|
||||
public UITheme Theme { get; internal set; }
|
||||
|
||||
private UITheme? _themeOverride;
|
||||
@@ -381,8 +384,6 @@ namespace Robust.Client.UserInterface
|
||||
/// </summary>
|
||||
public event EventHandler? OnShowTooltip;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// If this control is currently showing a tooltip provided via TooltipSupplier,
|
||||
/// returns that tooltip. Do not move this control within the tree, it should remain in PopupRoot.
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
private ReadOnlyMemory<char> _textMemory;
|
||||
private bool _clipText;
|
||||
private AlignMode _align;
|
||||
private Font? _fontOverride;
|
||||
|
||||
public Label()
|
||||
{
|
||||
@@ -106,7 +107,16 @@ namespace Robust.Client.UserInterface.Controls
|
||||
|
||||
[ViewVariables] public VAlignMode VAlign { get; set; }
|
||||
|
||||
public Font? FontOverride { get; set; }
|
||||
public Font? FontOverride
|
||||
{
|
||||
get => _fontOverride;
|
||||
set
|
||||
{
|
||||
_fontOverride = value;
|
||||
_textDimensionCacheValid = false;
|
||||
InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
|
||||
private Font ActualFont
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
@@ -20,9 +20,10 @@ namespace Robust.Client.UserInterface.Controls
|
||||
private readonly List<ButtonData> _buttonData = new();
|
||||
private readonly Dictionary<int, int> _idMap = new();
|
||||
private readonly Popup _popup;
|
||||
private readonly BoxContainer _popupVBox;
|
||||
private readonly BoxContainer _popupContentsBox;
|
||||
private readonly Label _label;
|
||||
private readonly TextureRect _triangle;
|
||||
private readonly LineEdit _filterBox;
|
||||
|
||||
public int ItemCount => _buttonData.Count;
|
||||
|
||||
@@ -39,6 +40,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
}
|
||||
private bool _hideTriangle;
|
||||
private bool _filterable;
|
||||
|
||||
/// <summary>
|
||||
/// StyleClasses to apply to the options that popup when clicking this button.
|
||||
@@ -50,6 +52,17 @@ namespace Robust.Client.UserInterface.Controls
|
||||
public string Prefix { get; set; } = string.Empty;
|
||||
public bool PrefixMargin { get; set; } = true;
|
||||
|
||||
public bool Filterable
|
||||
{
|
||||
get => _filterable;
|
||||
set
|
||||
{
|
||||
_filterable = value;
|
||||
_filterBox.Visible = value;
|
||||
UpdateFilters();
|
||||
}
|
||||
}
|
||||
|
||||
public OptionButton()
|
||||
{
|
||||
OptionStyleClasses = new List<string>();
|
||||
@@ -62,14 +75,26 @@ namespace Robust.Client.UserInterface.Controls
|
||||
};
|
||||
AddChild(hBox);
|
||||
|
||||
_popupVBox = new BoxContainer
|
||||
var popupVBox = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical
|
||||
Orientation = LayoutOrientation.Vertical, Children =
|
||||
{
|
||||
(_filterBox = new LineEdit
|
||||
{
|
||||
PlaceHolder = Loc.GetString("option-button-filter"),
|
||||
SelectAllOnFocus = true,
|
||||
Visible = false,
|
||||
}),
|
||||
(_popupContentsBox = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
OptionsScroll = new()
|
||||
{
|
||||
Children = { _popupVBox },
|
||||
Children = { popupVBox },
|
||||
ReturnMeasure = true,
|
||||
MaxHeight = 300
|
||||
};
|
||||
@@ -100,6 +125,11 @@ namespace Robust.Client.UserInterface.Controls
|
||||
Visible = !HideTriangle
|
||||
};
|
||||
hBox.AddChild(_triangle);
|
||||
|
||||
_filterBox.OnTextChanged += _ =>
|
||||
{
|
||||
UpdateFilters();
|
||||
};
|
||||
}
|
||||
|
||||
public void AddItem(Texture icon, string label, int? id = null)
|
||||
@@ -140,13 +170,14 @@ namespace Robust.Client.UserInterface.Controls
|
||||
};
|
||||
_idMap.Add(id.Value, _buttonData.Count);
|
||||
_buttonData.Add(data);
|
||||
_popupVBox.AddChild(button);
|
||||
_popupContentsBox.AddChild(button);
|
||||
if (_buttonData.Count == 1)
|
||||
{
|
||||
Select(0);
|
||||
}
|
||||
|
||||
ButtonOverride(button);
|
||||
UpdateFilter(data);
|
||||
}
|
||||
|
||||
private void TogglePopup(bool show)
|
||||
@@ -164,6 +195,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
var box = UIBox2.FromDimensions(globalPos, new Vector2(Math.Max(minX, Width), minY));
|
||||
Root.ModalRoot.AddChild(_popup);
|
||||
_popup.Open(box);
|
||||
|
||||
if (_filterable)
|
||||
_filterBox.GrabKeyboardFocus();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -201,7 +235,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
buttonDatum.Button.OnPressed -= ButtonOnPressed;
|
||||
}
|
||||
_buttonData.Clear();
|
||||
_popupVBox.DisposeAllChildren();
|
||||
_popupContentsBox.DisposeAllChildren();
|
||||
SelectedId = 0;
|
||||
}
|
||||
|
||||
@@ -229,7 +263,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
var data = _buttonData[idx];
|
||||
data.Button.OnPressed -= ButtonOnPressed;
|
||||
_idMap.Remove(data.Id);
|
||||
_popupVBox.RemoveChild(data.Button);
|
||||
_popupContentsBox.RemoveChild(data.Button);
|
||||
_buttonData.RemoveAt(idx);
|
||||
var newIdx = 0;
|
||||
foreach (var buttonData in _buttonData)
|
||||
@@ -330,6 +364,25 @@ namespace Robust.Client.UserInterface.Controls
|
||||
TogglePopup(false);
|
||||
}
|
||||
|
||||
private void UpdateFilters()
|
||||
{
|
||||
foreach (var entry in _buttonData)
|
||||
{
|
||||
UpdateFilter(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateFilter(ButtonData data)
|
||||
{
|
||||
if (!_filterable)
|
||||
{
|
||||
data.Button.Visible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
data.Button.Visible = data.Text.Contains(_filterBox.Text, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public sealed class ItemSelectedEventArgs : EventArgs
|
||||
{
|
||||
public OptionButton Button { get; }
|
||||
|
||||
@@ -266,7 +266,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return _getStyleBox()?.MinimumSize ?? Vector2.Zero;
|
||||
}
|
||||
|
||||
private void _invalidateEntries()
|
||||
internal void _invalidateEntries()
|
||||
{
|
||||
_totalContentHeight = 0;
|
||||
var font = _getFont();
|
||||
@@ -336,6 +336,14 @@ namespace Robust.Client.UserInterface.Controls
|
||||
base.UIScaleChanged();
|
||||
}
|
||||
|
||||
protected override void StylePropertiesChanged()
|
||||
{
|
||||
base.StylePropertiesChanged();
|
||||
|
||||
// Font may have changed.
|
||||
_invalidateEntries();
|
||||
}
|
||||
|
||||
internal static float GetScrollSpeed(Font font, float scale)
|
||||
{
|
||||
return font.GetLineHeight(scale) * 2;
|
||||
|
||||
@@ -6,6 +6,7 @@ using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Audio.Sources;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
@@ -157,6 +158,14 @@ namespace Robust.Client.UserInterface
|
||||
/// Render a control and all of its children.
|
||||
/// </summary>
|
||||
void RenderControl(IRenderHandle handle, Control control, Vector2i position);
|
||||
|
||||
/// <summary>
|
||||
/// Sawmill for use by controls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Exists so that control don't have to inject dependencies or otherwise obtain an <see cref="ILogManager"/> instance just to log errors.
|
||||
/// </remarks>
|
||||
ISawmill ControlSawmill { get; }
|
||||
}
|
||||
|
||||
public readonly struct PostDrawUIRootEventArgs
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -10,6 +11,7 @@ public sealed partial class FontPrototype : IPrototype
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
[Obsolete("Font prototype is a bad API.")]
|
||||
[DataField("path", required: true)]
|
||||
public ResPath Path { get; private set; } = default!;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
@@ -40,12 +41,31 @@ public sealed class FontTag : IMarkupTagHandler
|
||||
/// Creates the a vector font from the supplied font id.<br/>
|
||||
/// The size of the resulting font will be either the size supplied as a parameter to the tag, the previous font size or 12
|
||||
/// </summary>
|
||||
[Obsolete("Stop using font prototypes")]
|
||||
public static Font CreateFont(
|
||||
Stack<Font> contextFontStack,
|
||||
MarkupNode node,
|
||||
IResourceCache cache,
|
||||
IPrototypeManager prototypeManager,
|
||||
string fontId)
|
||||
{
|
||||
var size = GetSizeForFontTag(contextFontStack, node);
|
||||
|
||||
var hijack = IoCManager.Resolve<FontTagHijackHolder>();
|
||||
if (hijack.Hijack?.Invoke(fontId, size) is { } overriden)
|
||||
return overriden;
|
||||
|
||||
if (!prototypeManager.TryIndex<FontPrototype>(fontId, out var prototype))
|
||||
prototype = prototypeManager.Index<FontPrototype>(DefaultFont);
|
||||
|
||||
var fontResource = cache.GetResource<FontResource>(prototype.Path);
|
||||
return new VectorFont(fontResource, size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the desired font size for the given markup node.
|
||||
/// </summary>
|
||||
public static int GetSizeForFontTag(Stack<Font> contextFontStack, MarkupNode node)
|
||||
{
|
||||
var size = DefaultSize;
|
||||
|
||||
@@ -68,10 +88,6 @@ public sealed class FontTag : IMarkupTagHandler
|
||||
if (node.Attributes.TryGetValue("size", out var sizeParameter))
|
||||
size = (int) (sizeParameter.LongValue ?? size);
|
||||
|
||||
if (!prototypeManager.TryIndex<FontPrototype>(fontId, out var prototype))
|
||||
prototype = prototypeManager.Index<FontPrototype>(DefaultFont);
|
||||
|
||||
var fontResource = cache.GetResource<FontResource>(prototype.Path);
|
||||
return new VectorFont(fontResource, size);
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
53
Robust.Client/UserInterface/RichText/FontTagHijackHolder.cs
Normal file
53
Robust.Client/UserInterface/RichText/FontTagHijackHolder.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Robust.Client.UserInterface.RichText;
|
||||
|
||||
/// <returns>The font to replace the lookup with. Return null to fall back to default behavior.</returns>
|
||||
/// <seealso cref="FontTagHijackHolder"/>
|
||||
public delegate Font? FontTagHijack(ProtoId<FontPrototype> protoId, int size);
|
||||
|
||||
/// <summary>
|
||||
/// Allows replacing font resolution done by <see cref="FontPrototype"/>
|
||||
/// </summary>
|
||||
public sealed class FontTagHijackHolder
|
||||
{
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Called when a font prototype gets resolved.
|
||||
/// </summary>
|
||||
public FontTagHijack? Hijack;
|
||||
|
||||
/// <summary>
|
||||
/// Indicate that the results of <see cref="Hijack"/> may have changed,
|
||||
/// and that engine things relying on it must be updated.
|
||||
/// </summary>
|
||||
public void HijackUpdated()
|
||||
{
|
||||
// This isn't fool-proof, but it's probably good enough.
|
||||
// Recursively navigate the UI tree and invalidate rich text controls.
|
||||
var queue = new Queue<Control>();
|
||||
|
||||
foreach (var root in _ui.AllRoots)
|
||||
{
|
||||
queue.Enqueue(root);
|
||||
}
|
||||
|
||||
while (queue.TryDequeue(out var control))
|
||||
{
|
||||
foreach (var child in control.Children)
|
||||
{
|
||||
queue.Enqueue(child);
|
||||
}
|
||||
|
||||
if (control is OutputPanel output)
|
||||
output._invalidateEntries();
|
||||
else if (control is RichTextLabel label)
|
||||
label.InvalidateMeasure();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,13 @@ public sealed class MarkupTagManager
|
||||
{
|
||||
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
|
||||
[Dependency] private readonly ISandboxHelper _sandboxHelper = default!;
|
||||
[Dependency] private readonly IDependencyCollection _deps = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Tags defined in engine need to be instantiated here because of sandboxing
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, IMarkupTagHandler> _markupTagTypes = new IMarkupTagHandler[] {
|
||||
private readonly Dictionary<string, IMarkupTagHandler> _markupTagTypes = new IMarkupTagHandler[]
|
||||
{
|
||||
new BoldItalicTag(),
|
||||
new BoldTag(),
|
||||
new BulletTag(),
|
||||
@@ -50,13 +52,13 @@ public sealed class MarkupTagManager
|
||||
if (_engineTypes.Contains(type))
|
||||
continue;
|
||||
|
||||
var instance = (IMarkupTagHandler)_sandboxHelper.CreateInstance(type);
|
||||
var instance = (IMarkupTagHandler) _sandboxHelper.CreateInstance(type);
|
||||
_markupTagTypes[instance.Name.ToLower()] = instance;
|
||||
}
|
||||
|
||||
foreach (var (_, tag) in _markupTagTypes)
|
||||
foreach (var tag in _markupTagTypes.Values)
|
||||
{
|
||||
IoCManager.InjectDependencies(tag);
|
||||
_deps.InjectDependencies(tag);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -254,8 +254,13 @@ namespace Robust.Client.UserInterface
|
||||
control.Visible = true;
|
||||
|
||||
var invertedScale = 1f / uiScale;
|
||||
control.Position = new Vector2(baseLine.X * invertedScale, (baseLine.Y - defaultFont.GetAscent(uiScale)) * invertedScale);
|
||||
control.Measure(new Vector2(Width, Height));
|
||||
control.Arrange(UIBox2.FromDimensions(
|
||||
baseLine.X * invertedScale,
|
||||
(baseLine.Y - defaultFont.GetAscent(uiScale)) * invertedScale,
|
||||
control.DesiredSize.X,
|
||||
control.DesiredSize.Y
|
||||
));
|
||||
var advanceX = control.DesiredPixelSize.X;
|
||||
controlYAdvance = Math.Max(0f, (control.DesiredPixelSize.Y - GetLineHeight(font, uiScale, lineHeightScale)) * invertedScale);
|
||||
baseLine += new Vector2(advanceX, 0);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
@@ -97,11 +98,16 @@ internal partial class UserInterfaceManager
|
||||
}, true);
|
||||
}
|
||||
|
||||
internal static float CalculateUIScale(float osScale, IConfigurationManager cfg)
|
||||
{
|
||||
var cfgScale = cfg.GetCVar(CVars.DisplayUIScale);
|
||||
return cfgScale == 0 ? osScale : cfgScale;
|
||||
}
|
||||
|
||||
private float CalculateAutoScale(WindowRoot root)
|
||||
{
|
||||
//Grab the OS UIScale or the value set through CVAR debug
|
||||
var osScale = _configurationManager.GetCVar(CVars.DisplayUIScale);
|
||||
osScale = osScale == 0f ? root.Window.ContentScale.X : osScale;
|
||||
var osScale = CalculateUIScale(root.Window.ContentScale.X, _configurationManager);
|
||||
|
||||
var windowSize = root.Window.RenderTarget.Size;
|
||||
//Only run autoscale if it is enabled, otherwise default to just use OS UIScale
|
||||
|
||||
@@ -29,6 +29,11 @@ namespace Robust.Client.UserInterface
|
||||
{
|
||||
internal sealed partial class UserInterfaceManager : IUserInterfaceManagerInternal
|
||||
{
|
||||
/// <summary>
|
||||
/// A type that will always be instantiated anyways.
|
||||
/// </summary>
|
||||
public static readonly Type XamlHotReloadWarmupType = typeof(DropDownDebugConsole);
|
||||
|
||||
[Dependency] private readonly IDependencyCollection _rootDependencies = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IFontManager _fontManager = default!;
|
||||
@@ -95,6 +100,7 @@ namespace Robust.Client.UserInterface
|
||||
private Stylesheet? _stylesheet;
|
||||
|
||||
private ISawmill _sawmillUI = default!;
|
||||
public ISawmill ControlSawmill { get; private set; } = default!;
|
||||
|
||||
public event Action<Control>? OnKeyBindDown;
|
||||
|
||||
@@ -142,6 +148,7 @@ namespace Robust.Client.UserInterface
|
||||
private void _initializeCommon()
|
||||
{
|
||||
_sawmillUI = _logManager.GetSawmill("ui");
|
||||
ControlSawmill = _logManager.GetSawmill("ctrl");
|
||||
|
||||
RootControl = CreateWindowRoot(_clyde.MainWindow);
|
||||
RootControl.Name = "MainWindowRoot";
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Xaml;
|
||||
|
||||
namespace Robust.Client.UserInterface.XAML.Proxy;
|
||||
@@ -36,6 +39,8 @@ internal sealed class XamlImplementationStorage
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, Type> _fileType = new();
|
||||
|
||||
private readonly Dictionary<Type, string> _fileTypeReverse = new();
|
||||
|
||||
/// <summary>
|
||||
/// For each type, store the JIT-compiled implementation of Populate.
|
||||
/// </summary>
|
||||
@@ -50,6 +55,8 @@ internal sealed class XamlImplementationStorage
|
||||
private readonly ISawmill _sawmill;
|
||||
private readonly XamlJitDelegate _jitDelegate;
|
||||
|
||||
private readonly Lock _compileLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Create the storage.
|
||||
/// </summary>
|
||||
@@ -102,6 +109,8 @@ internal sealed class XamlImplementationStorage
|
||||
/// <param name="assembly">an assembly</param>
|
||||
public void Add(Assembly assembly)
|
||||
{
|
||||
using var _ = _compileLock.EnterScope();
|
||||
|
||||
foreach (var (type, metadata) in TypesWithXamlMetadata(assembly))
|
||||
{
|
||||
// this can fail, but if it does, that means something is _really_ wrong
|
||||
@@ -132,6 +141,8 @@ internal sealed class XamlImplementationStorage
|
||||
$"{fileName}. ({type.FullName} and {_fileType[fileName].FullName}). this is a bug in XamlAotCompiler"
|
||||
);
|
||||
}
|
||||
|
||||
_fileTypeReverse.Add(type, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,6 +156,8 @@ internal sealed class XamlImplementationStorage
|
||||
/// </remarks>
|
||||
public void ForceReloadAll()
|
||||
{
|
||||
using var _ = _compileLock.EnterScope();
|
||||
|
||||
foreach (var (fileName, fileContent) in _fileContent)
|
||||
{
|
||||
SetImplementation(fileName, fileContent, true);
|
||||
@@ -161,9 +174,19 @@ internal sealed class XamlImplementationStorage
|
||||
/// <returns>true if not a no-op</returns>
|
||||
public bool CanSetImplementation(string fileName)
|
||||
{
|
||||
using var _ = _compileLock.EnterScope();
|
||||
return _fileType.ContainsKey(fileName);
|
||||
}
|
||||
|
||||
public MethodInfo? CompileType(Type type)
|
||||
{
|
||||
if (_fileTypeReverse.TryGetValue(type, out var fileName))
|
||||
return SetImplementation(fileName, _fileContent[fileName], quiet: true);
|
||||
|
||||
_sawmill.Warning($"Type {type} has no XAML file!");
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace the implementation of <paramref name="fileName"/> by JIT-ing
|
||||
/// <paramref name="fileContent"/>.
|
||||
@@ -174,12 +197,14 @@ internal sealed class XamlImplementationStorage
|
||||
/// <param name="fileName">the name of the file whose implementation should be replaced</param>
|
||||
/// <param name="fileContent">the new implementation</param>
|
||||
/// <param name="quiet">if true, then don't bother to log</param>
|
||||
public void SetImplementation(string fileName, string fileContent, bool quiet)
|
||||
public MethodInfo? SetImplementation(string fileName, string fileContent, bool quiet)
|
||||
{
|
||||
using var _ = _compileLock.EnterScope();
|
||||
|
||||
if (!_fileType.TryGetValue(fileName, out var type))
|
||||
{
|
||||
_sawmill.Warning($"SetImplementation called with {fileName}, but no types care about its contents");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
var uri =
|
||||
@@ -190,12 +215,14 @@ internal sealed class XamlImplementationStorage
|
||||
{
|
||||
_sawmill.Debug($"replacing {fileName} for {type}");
|
||||
}
|
||||
|
||||
var impl = _jitDelegate(type, uri, fileName, fileContent);
|
||||
if (impl != null)
|
||||
{
|
||||
_populateImplementations[type] = impl;
|
||||
}
|
||||
_fileContent[fileName] = fileContent;
|
||||
return impl;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -210,8 +237,12 @@ internal sealed class XamlImplementationStorage
|
||||
{
|
||||
if (!_populateImplementations.TryGetValue(t, out var implementation))
|
||||
{
|
||||
// pop out if we never JITed anything
|
||||
return false;
|
||||
// JIT if needed.
|
||||
implementation = CompileType(t);
|
||||
|
||||
// pop out if we never JITed anything/couldn't JIT
|
||||
if (implementation == null)
|
||||
return false;
|
||||
}
|
||||
|
||||
implementation.Invoke(null, [null, o]);
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Reflection;
|
||||
@@ -17,6 +20,7 @@ public sealed class XamlProxyManager: IXamlProxyManager
|
||||
ISawmill _sawmill = null!;
|
||||
[Dependency] IReflectionManager _reflectionManager = null!;
|
||||
[Dependency] ILogManager _logManager = null!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = null!;
|
||||
|
||||
XamlImplementationStorage _xamlImplementationStorage = null!;
|
||||
|
||||
@@ -31,8 +35,21 @@ public sealed class XamlProxyManager: IXamlProxyManager
|
||||
_sawmill = _logManager.GetSawmill("xamlhotreload");
|
||||
_xamlImplementationStorage = new XamlImplementationStorage(_sawmill, Compile);
|
||||
|
||||
AddAssemblies();
|
||||
var preload = _cfg.GetCVar(CVars.UIXamlJitPreload);
|
||||
|
||||
AddAssemblies(reload: preload);
|
||||
_reflectionManager.OnAssemblyAdded += (_, _) => { AddAssemblies(); };
|
||||
|
||||
if (!preload)
|
||||
{
|
||||
// Compile any type at all on another thread, so we don't hold up main thread init with loading
|
||||
// the entire XAML compiler machinery.
|
||||
// In my testing, it took like 0.5s on debug to run the first XAML compile. Yeah.
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
_xamlImplementationStorage.CompileType(UserInterfaceManager.XamlHotReloadWarmupType);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -61,7 +78,7 @@ public sealed class XamlProxyManager: IXamlProxyManager
|
||||
/// Add all the types from all known assemblies, then force-JIT everything
|
||||
/// again.
|
||||
/// </summary>
|
||||
private void AddAssemblies()
|
||||
private void AddAssemblies(bool reload = true)
|
||||
{
|
||||
foreach (var a in _reflectionManager.Assemblies)
|
||||
{
|
||||
@@ -74,8 +91,8 @@ public sealed class XamlProxyManager: IXamlProxyManager
|
||||
}
|
||||
}
|
||||
|
||||
// Always use the JITed versions on debug builds
|
||||
_xamlImplementationStorage.ForceReloadAll();
|
||||
if (reload)
|
||||
_xamlImplementationStorage.ForceReloadAll();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -166,8 +166,12 @@ namespace Robust.Server
|
||||
public bool Start(ServerOptions options, Func<ILogHandler>? logHandlerFactory = null)
|
||||
{
|
||||
Options = options;
|
||||
|
||||
_config.Initialize(true);
|
||||
|
||||
_config.LoadCVarsFromAssembly(typeof(BaseServer).Assembly); // Robust.Server
|
||||
_config.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly); // Robust.Shared
|
||||
|
||||
if (Options.LoadConfigAndUserData)
|
||||
{
|
||||
string? path = _commandLineArgs?.ConfigFile;
|
||||
@@ -192,9 +196,6 @@ namespace Robust.Server
|
||||
}
|
||||
}
|
||||
|
||||
_config.LoadCVarsFromAssembly(typeof(BaseServer).Assembly); // Robust.Server
|
||||
_config.LoadCVarsFromAssembly(typeof(IConfigurationManager).Assembly); // Robust.Shared
|
||||
|
||||
CVarDefaultOverrides.OverrideServer(_config);
|
||||
|
||||
_config.OverrideConVars(EnvironmentVariables.GetEnvironmentCVars());
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -20,11 +22,11 @@ internal sealed partial class PvsSystem
|
||||
/// <returns>New entity State for the given entity.</returns>
|
||||
private EntityState GetEntityState(ICommonSession? player, EntityUid entityUid, GameTick fromTick, MetaDataComponent meta)
|
||||
{
|
||||
var bus = EntityManager.EventBus;
|
||||
var changed = new List<ComponentChange>();
|
||||
|
||||
bool sendCompList = meta.LastComponentRemoved > fromTick;
|
||||
HashSet<ushort>? netComps = sendCompList ? new() : null;
|
||||
var stateEv = new ComponentGetState(player, fromTick);
|
||||
|
||||
foreach (var (netId, component) in meta.NetComponents)
|
||||
{
|
||||
@@ -41,15 +43,15 @@ internal sealed partial class PvsSystem
|
||||
|
||||
if (component.LastModifiedTick <= fromTick)
|
||||
{
|
||||
if (sendCompList && (!component.SessionSpecific || player == null || EntityManager.CanGetComponentState(bus, component, player)))
|
||||
if (sendCompList && (!component.SessionSpecific || player == null || EntityManager.CanGetComponentState(component, player)))
|
||||
netComps!.Add(netId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (component.SessionSpecific && player != null && !EntityManager.CanGetComponentState(bus, component, player))
|
||||
if (component.SessionSpecific && player != null && !EntityManager.CanGetComponentState(component, player))
|
||||
continue;
|
||||
|
||||
var state = EntityManager.GetComponentState(bus, component, player, fromTick);
|
||||
var state = ComponentState(entityUid, component, netId, ref stateEv);
|
||||
changed.Add(new ComponentChange(netId, state, component.LastModifiedTick));
|
||||
|
||||
if (state != null)
|
||||
@@ -66,13 +68,23 @@ internal sealed partial class PvsSystem
|
||||
return entState;
|
||||
}
|
||||
|
||||
private IComponentState? ComponentState(EntityUid uid, IComponent comp, ushort netId, ref ComponentGetState stateEv)
|
||||
{
|
||||
DebugTools.Assert(comp.NetSyncEnabled, $"Attempting to get component state for an un-synced component: {comp.GetType()}");
|
||||
stateEv.State = null;
|
||||
_getStateHandlers![netId]?.Invoke(uid, comp, ref Unsafe.As<ComponentGetState, EntityEventBus.Unit>(ref stateEv));
|
||||
var state = stateEv.State;
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="GetEntityState"/> that includes all entity data, including data that can be inferred implicitly from the entity prototype.
|
||||
/// </summary>
|
||||
private EntityState GetFullEntityState(ICommonSession player, EntityUid entityUid, MetaDataComponent meta)
|
||||
{
|
||||
var bus = EntityManager.EventBus;
|
||||
var bus = EntityManager.EventBusInternal;
|
||||
var changed = new List<ComponentChange>();
|
||||
var stateEv = new ComponentGetState(player, GameTick.Zero);
|
||||
|
||||
HashSet<ushort> netComps = new();
|
||||
|
||||
@@ -86,7 +98,7 @@ internal sealed partial class PvsSystem
|
||||
if (component.SessionSpecific && !EntityManager.CanGetComponentState(bus, component, player))
|
||||
continue;
|
||||
|
||||
var state = EntityManager.GetComponentState(bus, component, player, GameTick.Zero);
|
||||
var state = ComponentState(entityUid, component, netId, ref stateEv);
|
||||
DebugTools.Assert(state is not IComponentDeltaState);
|
||||
changed.Add(new ComponentChange(netId, state, component.LastModifiedTick));
|
||||
netComps.Add(netId);
|
||||
|
||||
@@ -87,6 +87,9 @@ internal sealed partial class PvsSystem
|
||||
catch (Exception e)
|
||||
{
|
||||
_pvs.Log.Log(LogLevel.Error, e, $"Caught exception while processing pvs-leave messages.");
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ internal sealed partial class PvsSystem
|
||||
{
|
||||
var source = i >= 0 ? _sessions[i].Session.ToString() : "replays";
|
||||
Log.Log(LogLevel.Error, e, $"Caught exception while serializing game state for {source}.");
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ using Robust.Server.Replays;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
@@ -105,6 +106,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
private bool _async;
|
||||
|
||||
private DefaultObjectPool<PvsThreadResources> _threadResourcesPool = default!;
|
||||
private EntityEventBus.DirectedEventHandler?[]? _getStateHandlers;
|
||||
|
||||
private static readonly Histogram Histogram = Metrics.CreateHistogram("robust_game_state_update_usage",
|
||||
"Amount of time spent processing different parts of the game state update", new HistogramConfiguration
|
||||
@@ -173,6 +175,7 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
|
||||
ClearPvsData();
|
||||
ShutdownDirty();
|
||||
_getStateHandlers = null;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
@@ -185,6 +188,8 @@ internal sealed partial class PvsSystem : EntitySystem
|
||||
/// </summary>
|
||||
internal void SendGameStates(ICommonSession[] players)
|
||||
{
|
||||
_getStateHandlers ??= EntityManager.EventBusInternal.GetNetCompEventHandlers<ComponentGetState>();
|
||||
|
||||
// Wait for pending jobs and process disconnected players
|
||||
ProcessDisconnections();
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ namespace Robust.Shared.Maths
|
||||
[FieldOffset(sizeof(float) * 2)] public Vector2 TopRight;
|
||||
|
||||
[NonSerialized]
|
||||
[FieldOffset(sizeof(float) * 0)] public System.Numerics.Vector4 AsVector4;
|
||||
[FieldOffset(sizeof(float) * 0)] public Vector4 AsVector4;
|
||||
|
||||
public readonly Vector2 BottomRight
|
||||
{
|
||||
@@ -87,7 +87,7 @@ namespace Robust.Shared.Maths
|
||||
public readonly Vector2 Center
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => BottomLeft + Size * .5f;
|
||||
get => (BottomLeft + TopRight) * .5f;
|
||||
}
|
||||
|
||||
public readonly Vector2 Extents
|
||||
@@ -495,7 +495,7 @@ namespace Robust.Shared.Maths
|
||||
return new Vector2(cx, cy);
|
||||
}
|
||||
|
||||
public bool EqualsApprox(Box2 other)
|
||||
public readonly bool EqualsApprox(Box2 other)
|
||||
{
|
||||
return MathHelper.CloseToPercent(Left, other.Left)
|
||||
&& MathHelper.CloseToPercent(Bottom, other.Bottom)
|
||||
@@ -503,7 +503,7 @@ namespace Robust.Shared.Maths
|
||||
&& MathHelper.CloseToPercent(Top, other.Top);
|
||||
}
|
||||
|
||||
public bool EqualsApprox(Box2 other, double tolerance)
|
||||
public readonly bool EqualsApprox(Box2 other, double tolerance)
|
||||
{
|
||||
return MathHelper.CloseToPercent(Left, other.Left, tolerance)
|
||||
&& MathHelper.CloseToPercent(Bottom, other.Bottom, tolerance)
|
||||
|
||||
@@ -33,7 +33,31 @@ namespace Robust.Shared.Maths
|
||||
public readonly Vector2 BottomLeft => Origin + Rotation.RotateVec(Box.BottomLeft - Origin);
|
||||
public readonly Vector2 Center => Origin + Rotation.RotateVec((Box.BottomLeft + Box.TopRight)/2 - Origin);
|
||||
|
||||
public Matrix3x2 Transform => Matrix3Helpers.CreateTransform(Origin - Rotation.RotateVec(Origin), Rotation);
|
||||
public readonly Matrix3x2 Transform
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get
|
||||
{
|
||||
// Equivalent to Matrix3Helpers.CreateTransform(Origin - Rotation.RotateVec(Origin), Rotation)
|
||||
// but ~20% faster
|
||||
var angle = (float) Rotation;
|
||||
var sin = MathF.Sin(angle);
|
||||
var cos = MathF.Cos(angle);
|
||||
var cos1 = 1 - cos;
|
||||
var dx = cos1 * Origin.X + sin * Origin.Y;
|
||||
var dy = - sin * Origin.X + cos1 * Origin.Y;
|
||||
|
||||
return new Matrix3x2
|
||||
{
|
||||
M11 = cos,
|
||||
M12 = sin,
|
||||
M21 = -sin,
|
||||
M22 = cos,
|
||||
M31 = dx,
|
||||
M32 = dy,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public Box2Rotated(Vector2 bottomLeft, Vector2 topRight)
|
||||
: this(new Box2(bottomLeft, topRight))
|
||||
@@ -71,6 +95,19 @@ namespace Robust.Shared.Maths
|
||||
/// Calculates the smallest AABB that will encompass the rotated box. The AABB is in local space.
|
||||
/// </summary>
|
||||
public readonly Box2 CalcBoundingBox()
|
||||
{
|
||||
GetVertices(out var x, out var y);
|
||||
var aabb = SimdHelpers.GetAABB(x, y);
|
||||
return Unsafe.As<Vector128<float>, Box2>(ref aabb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the transformation to the box's corners and returns the coordinates in two simd vectors.
|
||||
/// </summary>
|
||||
/// <remarks>The corners are ordered clockwise, starting from what was the bottom left corner prior to the transformation.</remarks>
|
||||
/// <remarks>This is effectively a specialized variant of a <see cref="Matrix3Helpers"/> transform method that avoids having to use construct the matrix via <see cref="Transform"/></remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal readonly void GetVertices(out Vector128<float> x, out Vector128<float> y)
|
||||
{
|
||||
var boxVec = Unsafe.As<Box2, Vector128<float>>(ref Unsafe.AsRef(in Box));
|
||||
|
||||
@@ -80,37 +117,11 @@ namespace Robust.Shared.Maths
|
||||
var cos = Vector128.Create((float) Math.Cos(Rotation));
|
||||
var sin = Vector128.Create((float) Math.Sin(Rotation));
|
||||
|
||||
var allX = Vector128.Shuffle(boxVec, Vector128.Create(0, 0, 2, 2));
|
||||
var allY = Vector128.Shuffle(boxVec, Vector128.Create(1, 3, 3, 1));
|
||||
var boxX = Vector128.Shuffle(boxVec, Vector128.Create(0, 2, 2, 0)) - originX;
|
||||
var boxY = Vector128.Shuffle(boxVec, Vector128.Create(1, 1, 3, 3)) - originY;
|
||||
|
||||
allX -= originX;
|
||||
allY -= originY;
|
||||
|
||||
var modX = allX * cos - allY * sin;
|
||||
var modY = allX * sin + allY * cos;
|
||||
|
||||
allX = modX + originX;
|
||||
allY = modY + originY;
|
||||
|
||||
// lrlr = vector containing [left right left right]
|
||||
Vector128<float> lbrt;
|
||||
|
||||
if (Sse.IsSupported)
|
||||
{
|
||||
var lrlr = SimdHelpers.MinMaxHorizontalSse(allX);
|
||||
var btbt = SimdHelpers.MinMaxHorizontalSse(allY);
|
||||
lbrt = Sse.UnpackLow(lrlr, btbt);
|
||||
}
|
||||
else
|
||||
{
|
||||
var l = SimdHelpers.MinHorizontal128(allX);
|
||||
var b = SimdHelpers.MinHorizontal128(allY);
|
||||
var r = SimdHelpers.MaxHorizontal128(allX);
|
||||
var t = SimdHelpers.MaxHorizontal128(allY);
|
||||
lbrt = SimdHelpers.MergeRows128(l, b, r, t);
|
||||
}
|
||||
|
||||
return Unsafe.As<Vector128<float>, Box2>(ref lbrt);
|
||||
x = boxX * cos - boxY * sin + originX;
|
||||
y = boxX * sin + boxY * cos + originY;
|
||||
}
|
||||
|
||||
public readonly bool Contains(Vector2 worldPoint)
|
||||
@@ -133,7 +144,21 @@ namespace Robust.Shared.Maths
|
||||
/// <inheritdoc />
|
||||
public readonly bool Equals(Box2Rotated other)
|
||||
{
|
||||
return Box.Equals(other.Box) && Rotation.Equals(other.Rotation);
|
||||
return Box.Equals(other.Box) && Rotation.Equals(other.Rotation) && Origin.Equals(other.Origin);
|
||||
}
|
||||
|
||||
public readonly bool EqualsApprox(Box2Rotated other)
|
||||
{
|
||||
return Box.EqualsApprox(other.Box)
|
||||
&& Rotation.EqualsApprox(other.Rotation)
|
||||
&& Origin.EqualsApprox(other.Origin);
|
||||
}
|
||||
|
||||
public readonly bool EqualsApprox(Box2Rotated other, double tolerance)
|
||||
{
|
||||
return Box.EqualsApprox(other.Box, tolerance)
|
||||
&& Rotation.EqualsApprox(other.Rotation, tolerance)
|
||||
&& Origin.EqualsApprox(other.Origin, tolerance);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -107,7 +107,6 @@ namespace Robust.Shared.Maths
|
||||
/// <param name="a">The alpha component of the new Color structure.</param>
|
||||
public Color(byte r, byte g, byte b, byte a = 255)
|
||||
{
|
||||
Unsafe.SkipInit(out this);
|
||||
R = r / (float) byte.MaxValue;
|
||||
G = g / (float) byte.MaxValue;
|
||||
B = b / (float) byte.MaxValue;
|
||||
|
||||
@@ -15,6 +15,7 @@ using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
|
||||
namespace Robust.Shared.Maths
|
||||
{
|
||||
@@ -579,6 +580,17 @@ namespace Robust.Shared.Maths
|
||||
return Math.Abs(a - b) <= epsilon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether two floating point numbers are within <paramref name="percentage"/> of eachother
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool CloseToPercent(Vector128<float> a, Vector128<float> b, float percentage = .00001f)
|
||||
{
|
||||
var epsilon = Vector128.Max(Vector128.Max(Vector128.Abs(a), Vector128.Abs(b)) * Vector128.Create(percentage),
|
||||
Vector128.Create(percentage));
|
||||
var result = Vector128.LessThanOrEqual(Vector128.Abs(a - b), epsilon);
|
||||
return Vector128.EqualsAll(result.AsInt32(), Vector128<int>.AllBitsSet);
|
||||
}
|
||||
#endregion CloseToPercent
|
||||
|
||||
#region CloseTo
|
||||
|
||||
@@ -3,7 +3,6 @@ using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
|
||||
|
||||
namespace Robust.Shared.Maths;
|
||||
|
||||
public static class Matrix3Helpers
|
||||
@@ -33,38 +32,52 @@ public static class Matrix3Helpers
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Box2 TransformBox(this Matrix3x2 refFromBox, Box2Rotated box)
|
||||
public static Box2 TransformBox(this Matrix3x2 refFromBox, in Box2Rotated box)
|
||||
{
|
||||
return (box.Transform * refFromBox).TransformBox(box.Box);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static void TransformBox(
|
||||
this Matrix3x2 refFromBox,
|
||||
in Box2Rotated box,
|
||||
out Vector128<float> x,
|
||||
out Vector128<float> y)
|
||||
{
|
||||
(box.Transform * refFromBox).TransformBox(box.Box, out x, out y);
|
||||
}
|
||||
|
||||
public static Box2 TransformBox(this Matrix3x2 refFromBox, in Box2 box)
|
||||
{
|
||||
// Do transformation on all 4 corners of the box at once.
|
||||
// Then min/max the results to get the new AABB.
|
||||
TransformBox(refFromBox, box, out var x, out var y);
|
||||
var aabb = SimdHelpers.GetAABB(x, y);
|
||||
return Unsafe.As<Vector128<float>, Box2>(ref aabb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a transformation matrix to all of a box's corners and returns their coordinates in two simd vectors.
|
||||
/// </summary>
|
||||
/// <remarks>The corners are ordered clockwise, starting from what was the bottom left corner prior to the transformation.</remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
internal static void TransformBox(
|
||||
this Matrix3x2 refFromBox,
|
||||
in Box2 box,
|
||||
out Vector128<float> x,
|
||||
out Vector128<float> y)
|
||||
{
|
||||
var boxVec = Unsafe.As<Box2, Vector128<float>>(ref Unsafe.AsRef(in box));
|
||||
|
||||
// Convert box into list of X and Y values for each of the 4 corners
|
||||
var allX = Vector128.Shuffle(boxVec, Vector128.Create(0, 0, 2, 2));
|
||||
var allY = Vector128.Shuffle(boxVec, Vector128.Create(1, 3, 3, 1));
|
||||
var boxX = Vector128.Shuffle(boxVec, Vector128.Create(0, 2, 2, 0));
|
||||
var boxY = Vector128.Shuffle(boxVec, Vector128.Create(1, 1, 3, 3));
|
||||
|
||||
// Transform coordinates
|
||||
var modX = allX * Vector128.Create(refFromBox.M11);
|
||||
var modY = allX * Vector128.Create(refFromBox.M12);
|
||||
modX += allY * Vector128.Create(refFromBox.M21);
|
||||
modY += allY * Vector128.Create(refFromBox.M22);
|
||||
modX += Vector128.Create(refFromBox.M31);
|
||||
modY += Vector128.Create(refFromBox.M32);
|
||||
|
||||
// Get bounding box by finding the min and max X and Y values.
|
||||
var l = SimdHelpers.MinHorizontal128(modX);
|
||||
var b = SimdHelpers.MinHorizontal128(modY);
|
||||
var r = SimdHelpers.MaxHorizontal128(modX);
|
||||
var t = SimdHelpers.MaxHorizontal128(modY);
|
||||
|
||||
var lbrt = SimdHelpers.MergeRows128(l, b, r, t);
|
||||
return Unsafe.As<Vector128<float>, Box2>(ref lbrt);
|
||||
x = Vector128.Create(refFromBox.M31)
|
||||
+ boxX * Vector128.Create(refFromBox.M11)
|
||||
+ boxY * Vector128.Create(refFromBox.M21);
|
||||
y = Vector128.Create(refFromBox.M32)
|
||||
+ boxX * Vector128.Create(refFromBox.M12)
|
||||
+ boxY * Vector128.Create(refFromBox.M22);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -76,6 +89,23 @@ public static class Matrix3Helpers
|
||||
return new Angle(Math.Atan2(t.M12, t.M11));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Matrix3x2 CreateTransform(float posX, float posY, double angle)
|
||||
{
|
||||
// returns a matrix that is equivalent to returning CreateRotation(angle) * CreateTranslation(posX, posY)
|
||||
var sin = (float) Math.Sin(angle);
|
||||
var cos = (float) Math.Cos(angle);
|
||||
return new Matrix3x2
|
||||
{
|
||||
M11 = cos,
|
||||
M21 = -sin,
|
||||
M31 = posX,
|
||||
M12 = sin,
|
||||
M22 = cos,
|
||||
M32 = posY,
|
||||
};
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Matrix3x2 CreateTransform(float posX, float posY, double angle, float scaleX = 1, float scaleY = 1)
|
||||
{
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
[module: SkipLocalsInit]
|
||||
#endif
|
||||
|
||||
[assembly: InternalsVisibleTo("Robust.Shared")]
|
||||
[assembly: InternalsVisibleTo("Robust.Server")]
|
||||
[assembly: InternalsVisibleTo("Robust.Client")]
|
||||
[assembly: InternalsVisibleTo("Robust.UnitTesting")]
|
||||
|
||||
#if DEVELOPMENT
|
||||
[assembly: InternalsVisibleTo("Robust.Benchmarks")]
|
||||
[assembly: InternalsVisibleTo("Content.Benchmarks")]
|
||||
#endif
|
||||
|
||||
@@ -9,34 +9,6 @@ namespace Robust.Shared.Maths
|
||||
/// </summary>
|
||||
internal static class SimdHelpers
|
||||
{
|
||||
/// <returns>A vector with the horizontal minimum and maximum values arranged as { min max min max} .</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Vector128<float> MinMaxHorizontalSse(Vector128<float> input)
|
||||
{
|
||||
var tmp = Sse.Shuffle(input, input, 0b00_01_10_11);
|
||||
var min = Sse.Min(tmp, input);
|
||||
var max = Sse.Max(tmp, input);
|
||||
tmp = Sse.Shuffle(min, max, 0b01_00_00_01);
|
||||
min = Sse.Min(tmp, min);
|
||||
max = Sse.Max(tmp, max);
|
||||
tmp = Sse.MoveScalar(max, min); // no generic Vector128 equivalent :(
|
||||
return Sse.Shuffle(tmp, tmp, 0b11_00_11_00);
|
||||
}
|
||||
|
||||
/// <returns>A vector with the horizontal minimum and maximum values arranged as { max min max min} .</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Vector128<float> MaxMinHorizontalSse(Vector128<float> input)
|
||||
{
|
||||
var tmp = Sse.Shuffle(input, input, 0b00_01_10_11);
|
||||
var min = Sse.Min(tmp, input);
|
||||
var max = Sse.Max(tmp, input);
|
||||
tmp = Sse.Shuffle(min, max, 0b01_00_00_01);
|
||||
min = Sse.Min(tmp, min);
|
||||
max = Sse.Max(tmp, max);
|
||||
tmp = Sse.MoveScalar(max, min); // no generic Vector128 equivalent :(
|
||||
return Sse.Shuffle(tmp, tmp, 0b00_11_00_11);
|
||||
}
|
||||
|
||||
/// <returns>The min value is broadcast to the whole vector.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Vector128<float> MinHorizontal128(Vector128<float> v)
|
||||
@@ -79,6 +51,84 @@ namespace Robust.Shared.Maths
|
||||
return n + d;
|
||||
}
|
||||
|
||||
#region GetAABB
|
||||
|
||||
/// <summary>
|
||||
/// This computes the bounding box given a set of 4 coordinates specified via 2 simd vectors.
|
||||
/// This effectively computes the horizontal min & max of both of the given vectors.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Returns a simd vector that can be directly cast to a <see cref="Box2"/>.
|
||||
/// </remarks>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Vector128<float> GetAABB(Vector128<float> x, Vector128<float> y)
|
||||
{
|
||||
return Avx.IsSupported ? GetAABBAvx(x, y) : GetAABBSlow(x, y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This computes the bounding box given a set of 4 coordinates specified via 2 simd vectors.
|
||||
/// This effectively computes the horizontal min & max of both of the given vectors.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Vector128<float> GetAABBAvx(Vector128<float> x, Vector128<float> y)
|
||||
{
|
||||
// This can be turned into a 256 bit version that only needs 4 min/max instead of 6
|
||||
// But the performance difference seems negligible.
|
||||
|
||||
// x = [x0, x1, x2, x3]
|
||||
// y = [y0, y1, y2, y3]
|
||||
|
||||
var xmin = Vector128.Shuffle(x, Vector128.Create(1, 0, 3, 2));
|
||||
xmin = Sse.Min(xmin, x);
|
||||
// xmin = [min(x0,x1), min(x0,x1), min(x2,x3), min(x2,x3)]
|
||||
|
||||
var ymin = Vector128.Shuffle(y, Vector128.Create(1, 0, 3, 2));
|
||||
ymin = Sse.Min(ymin, y);
|
||||
// ymin = [min(y0,y1), min(y0,x1), min(y2,y3), min(y2,y3)]
|
||||
|
||||
var xymin = Sse41.Blend(xmin, ymin, 0b_1_0_1_0);
|
||||
// xymin = [min(x0,x1), min(y0,y1), min(x2,x3), min(y2,y3)]
|
||||
|
||||
var xyminPermuted = Avx.Permute(xymin, 0b_00_00_11_10);
|
||||
// xymin_permuted = [min(x2,x3), min(y2,y3), ..., ... ]
|
||||
|
||||
var min = Sse.Min(xymin, xyminPermuted);
|
||||
// min = [min(x0,x1,x2,x3), min(y0,y1,y2,y3), ..., ... ]
|
||||
|
||||
var xmax = Vector128.Shuffle(x, Vector128.Create(1, 0, 3, 2));
|
||||
xmax = Sse.Max(xmax, x);
|
||||
// xmax = [max(x0,x1), max(x0,x1), max(x2,x3), max(x2,x3)]
|
||||
|
||||
var ymax = Vector128.Shuffle(y, Vector128.Create(1, 0, 3, 2));
|
||||
ymax = Sse.Max(ymax, y);
|
||||
// ymax = [max(y0,y1), max(y0,y1), max(y2,y3), max(y2,y3)]
|
||||
|
||||
var xymax = Sse41.Blend(xmax, ymax, 0b_1_0_1_0);
|
||||
// xymax = [max(x0,x1), max(y0,y1), max(x2,x3), max(y2,y3)]
|
||||
|
||||
var xymaxPermuted = Avx.Permute(xymax, 0b_01_00_00_00);
|
||||
// xymax_permuted = [.., .., max(x0,x1), max(y0,y1) ]
|
||||
|
||||
var max = Sse.Max(xymax, xymaxPermuted);
|
||||
// max = [.., .., max(x0,x1,x2,x3), max(y0,y1,y2,y3) ]
|
||||
|
||||
// result = [min(x0,x1,x2,x3), min(y0,y1,y2,y3), max(x0,x1,x2,x3), max(y0,y1,y2,y3) ]
|
||||
return Sse41.Blend(min, max, 0b_1_1_0_0);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Vector128<float> GetAABBSlow(Vector128<float> x, Vector128<float> y)
|
||||
{
|
||||
var l = MinHorizontal128(x);
|
||||
var b = MinHorizontal128(y);
|
||||
var r = MaxHorizontal128(x);
|
||||
var t = MaxHorizontal128(y);
|
||||
return MergeRows128(l, b, r, t);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// Given the following vectors:
|
||||
// x: X X X X
|
||||
// y: Y Y Y Y
|
||||
|
||||
@@ -1280,13 +1280,6 @@ namespace Robust.Shared
|
||||
* PHYSICS
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// How much to expand broadphase checking for. This is useful for cross-grid collisions.
|
||||
/// Performance impact if additional broadphases are being checked.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> BroadphaseExpand =
|
||||
CVarDef.Create("physics.broadphase_expand", 2f, CVar.ARCHIVE | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// The target minimum ticks per second on the server.
|
||||
/// This is used for substepping and will help with clipping/physics issues and such.
|
||||
@@ -1836,6 +1829,15 @@ namespace Robust.Shared
|
||||
/// </remarks>
|
||||
public static readonly CVarDef<bool> CfgCheckUnused = CVarDef.Create("cfg.check_unused", true);
|
||||
|
||||
/// <summary>
|
||||
/// Storage for CVars that should be rolled back next client startup.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This CVar is utilized through <see cref="IConfigurationManager"/>'s rollback functionality.
|
||||
/// </remarks>
|
||||
internal static readonly CVarDef<string>
|
||||
CfgRollbackData = CVarDef.Create("cfg.rollback_data", "", CVar.ARCHIVE);
|
||||
|
||||
/*
|
||||
* Network Resource Manager
|
||||
*/
|
||||
@@ -1915,5 +1917,50 @@ namespace Robust.Shared
|
||||
/// </summary>
|
||||
public static readonly CVarDef<string> XamlHotReloadMarkerName =
|
||||
CVarDef.Create("ui.xaml_hot_reload_marker_name", "SpaceStation14.sln", CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// If true, all XAML UIs will be JITed for hot reload on client startup.
|
||||
/// If false, they will be JITed on demand.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> UIXamlJitPreload =
|
||||
CVarDef.Create("ui.xaml_jit_preload", false, CVar.CLIENTONLY);
|
||||
|
||||
/*
|
||||
* FONT
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// If false, disable system font support.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> FontSystem =
|
||||
CVarDef.Create("font.system", true, CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// If true, allow Windows "downloadable" fonts to be exposed to the system fonts API.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> FontWindowsDownloadable =
|
||||
CVarDef.Create("font.windows_downloadable", false, CVar.CLIENTONLY | CVar.ARCHIVE);
|
||||
|
||||
/*
|
||||
* LOADING
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Whether to show explicit loading bar during client initialization.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> LoadingShowBar =
|
||||
CVarDef.Create("loading.show_bar", true, CVar.CLIENTONLY);
|
||||
|
||||
#if TOOLS
|
||||
private const bool DefaultShowDebug = true;
|
||||
#else
|
||||
private const bool DefaultShowDebug = false;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// Whether to show "debug" info in the loading screen.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> LoadingShowDebug =
|
||||
CVarDef.Create("loading.show_debug", DefaultShowDebug, CVar.CLIENTONLY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Console;
|
||||
@@ -58,6 +59,25 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
internal static IEnumerable<CompletionOption> GetCVarCompletionOptions(IConfigurationManager cfg)
|
||||
{
|
||||
return cfg.GetRegisteredCVars()
|
||||
.Select(c => new CompletionOption(c, GetCVarValueHint(cfg, c)));
|
||||
}
|
||||
|
||||
private static string GetCVarValueHint(IConfigurationManager cfg, string cVar)
|
||||
{
|
||||
var flags = cfg.GetCVarFlags(cVar);
|
||||
if ((flags & CVar.CONFIDENTIAL) != 0)
|
||||
return Loc.GetString("cmd-cvar-value-hidden");
|
||||
|
||||
var value = cfg.GetCVar<object>(cVar).ToString() ?? "";
|
||||
if (value.Length > 50)
|
||||
value = $"{value[..51]}…";
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "StringLiteralTypo")]
|
||||
@@ -120,8 +140,7 @@ namespace Robust.Shared.Configuration
|
||||
var helpQuestion = Loc.GetString("cmd-cvar-compl-list");
|
||||
|
||||
return CompletionResult.FromHintOptions(
|
||||
_cfg.GetRegisteredCVars()
|
||||
.Select(c => new CompletionOption(c, GetCVarValueHint(_cfg, c)))
|
||||
CVarCommandUtil.GetCVarCompletionOptions(_cfg)
|
||||
.Union(new[] { new CompletionOption("?", helpQuestion) })
|
||||
.OrderBy(c => c.Value),
|
||||
Loc.GetString("cmd-cvar-arg-name"));
|
||||
@@ -134,19 +153,6 @@ namespace Robust.Shared.Configuration
|
||||
var type = _cfg.GetCVarType(cvar);
|
||||
return CompletionResult.FromHint($"<{type.Name}>");
|
||||
}
|
||||
|
||||
private string GetCVarValueHint(IConfigurationManager cfg, string cVar)
|
||||
{
|
||||
var flags = cfg.GetCVarFlags(cVar);
|
||||
if ((flags & CVar.CONFIDENTIAL) != 0)
|
||||
return Loc.GetString("cmd-cvar-value-hidden");
|
||||
|
||||
var value = cfg.GetCVar<object>(cVar).ToString() ?? "";
|
||||
if (value.Length > 50)
|
||||
value = $"{value[..51]}…";
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class CVarSubsCommand : LocalizedCommands
|
||||
@@ -191,4 +197,83 @@ namespace Robust.Shared.Configuration
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ConfigMarkRollbackCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = null!;
|
||||
|
||||
public string Command => "config_rollback_mark";
|
||||
public string Description => "";
|
||||
public string Help => "";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length is < 1 or > 2)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-invalid-arg-number-error"));
|
||||
return;
|
||||
}
|
||||
|
||||
_cfg.MarkForRollback(args[0]);
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromOptions(
|
||||
CVarCommandUtil.GetCVarCompletionOptions(_cfg)
|
||||
.OrderBy(c => c.Value));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ConfigUnmarkRollbackCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = null!;
|
||||
|
||||
public string Command => "config_rollback_unmark";
|
||||
public string Description => "";
|
||||
public string Help => "";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length is < 1 or > 2)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-invalid-arg-number-error"));
|
||||
return;
|
||||
}
|
||||
|
||||
_cfg.UnmarkForRollback(args[0]);
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromOptions(
|
||||
CVarCommandUtil.GetCVarCompletionOptions(_cfg)
|
||||
.OrderBy(c => c.Value));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal sealed class ConfigApplyRollbackCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = null!;
|
||||
|
||||
public string Command => "config_rollback_apply";
|
||||
public string Description => "";
|
||||
public string Help => "";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
_cfg.ApplyRollback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
95
Robust.Shared/Configuration/ConfigurationManager.Rollback.cs
Normal file
95
Robust.Shared/Configuration/ConfigurationManager.Rollback.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Nett;
|
||||
|
||||
namespace Robust.Shared.Configuration;
|
||||
|
||||
internal partial class ConfigurationManager
|
||||
{
|
||||
public void MarkForRollback(params CVarDef[] cVars)
|
||||
{
|
||||
MarkForRollback(cVars.Select(c => c.Name).ToArray());
|
||||
}
|
||||
|
||||
public void MarkForRollback(params string[] cVars)
|
||||
{
|
||||
var alreadyPending = LoadPendingRollbackTable() ?? [];
|
||||
|
||||
foreach (var cVar in cVars)
|
||||
{
|
||||
alreadyPending[cVar] = GetCVar(cVar);
|
||||
}
|
||||
|
||||
SavePendingRollbackTable(alreadyPending);
|
||||
}
|
||||
|
||||
public void UnmarkForRollback(params CVarDef[] cVars)
|
||||
{
|
||||
UnmarkForRollback(cVars.Select(c => c.Name).ToArray());
|
||||
}
|
||||
|
||||
public void UnmarkForRollback(params string[] cVars)
|
||||
{
|
||||
var alreadyPending = LoadPendingRollbackTable() ?? [];
|
||||
|
||||
foreach (var cVar in cVars)
|
||||
{
|
||||
alreadyPending.Remove(cVar);
|
||||
}
|
||||
|
||||
SavePendingRollbackTable(alreadyPending);
|
||||
}
|
||||
|
||||
private void SavePendingRollbackTable(Dictionary<string, object> pending)
|
||||
{
|
||||
var tbl = SaveToTomlTable(pending.Keys, cVar => pending[cVar]);
|
||||
var str = Toml.WriteString(tbl);
|
||||
SetCVar(CVars.CfgRollbackData, str);
|
||||
}
|
||||
|
||||
public void ApplyRollback()
|
||||
{
|
||||
var rollbackValue = GetCVar(CVars.CfgRollbackData);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rollbackValue))
|
||||
return;
|
||||
|
||||
_sawmill.Debug("We have CVars to roll back!");
|
||||
|
||||
try
|
||||
{
|
||||
var tblRoot = Toml.ReadString(rollbackValue);
|
||||
var loaded = LoadFromTomlTable(tblRoot);
|
||||
_sawmill.Info($"Rolled back CVars: {string.Join(", ", loaded)}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Failed to load rollback data:\n{e}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetCVar(CVars.CfgRollbackData, "");
|
||||
SaveToFile();
|
||||
}
|
||||
}
|
||||
|
||||
private Dictionary<string, object>? LoadPendingRollbackTable()
|
||||
{
|
||||
var rollbackValue = GetCVar(CVars.CfgRollbackData);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rollbackValue))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var tblRoot = Toml.ReadString(rollbackValue);
|
||||
return ParseCVarValuesFromToml(tblRoot).ToDictionary();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Failed to load rollback data:\n{e}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Robust.Shared.Configuration
|
||||
/// Stores and manages global configuration variables.
|
||||
/// </summary>
|
||||
[Virtual]
|
||||
internal class ConfigurationManager : IConfigurationManagerInternal
|
||||
internal partial class ConfigurationManager : IConfigurationManagerInternal
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
@@ -60,36 +60,53 @@ namespace Robust.Shared.Configuration
|
||||
/// <inheritdoc />
|
||||
public HashSet<string> LoadFromTomlStream(Stream file)
|
||||
{
|
||||
var loaded = new HashSet<string>();
|
||||
TomlTable tblRoot;
|
||||
try
|
||||
{
|
||||
var callbackEvents = new ValueList<ValueChangedInvoke>();
|
||||
|
||||
// Ensure callbacks are raised OUTSIDE the write lock.
|
||||
using (Lock.WriteGuard())
|
||||
{
|
||||
foreach (var (cvar, value) in ParseCVarValuesFromToml(file))
|
||||
{
|
||||
loaded.Add(cvar);
|
||||
LoadTomlVar(cvar, value, ref callbackEvents);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var callback in callbackEvents)
|
||||
{
|
||||
InvokeValueChanged(callback);
|
||||
}
|
||||
tblRoot = Toml.ReadStream(file);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
loaded.Clear();
|
||||
_sawmill.Error("Unable to load configuration from stream:\n{0}", e);
|
||||
_sawmill.Error("Unable to load configuration from table:\n{0}", e);
|
||||
return [];
|
||||
}
|
||||
|
||||
return LoadFromTomlTable(tblRoot);
|
||||
}
|
||||
|
||||
private HashSet<string> LoadFromTomlTable(TomlTable table)
|
||||
{
|
||||
var loaded = new HashSet<string>();
|
||||
var callbackEvents = new ValueList<ValueChangedInvoke>();
|
||||
try
|
||||
{
|
||||
// Ensure callbacks are raised OUTSIDE the write lock.
|
||||
using (Lock.WriteGuard())
|
||||
{
|
||||
foreach (var (cvar, value) in ParseCVarValuesFromToml(table))
|
||||
{
|
||||
loaded.Add(cvar);
|
||||
LoadParsedVar(cvar, value, ref callbackEvents);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
RunDeferredInvokeCallbacks(in callbackEvents);
|
||||
}
|
||||
|
||||
return loaded;
|
||||
}
|
||||
|
||||
private void LoadTomlVar(
|
||||
private void RunDeferredInvokeCallbacks(in ValueList<ValueChangedInvoke> callbackEvents)
|
||||
{
|
||||
foreach (var callback in callbackEvents)
|
||||
{
|
||||
InvokeValueChanged(callback);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadParsedVar(
|
||||
string cvar,
|
||||
object value,
|
||||
ref ValueList<ValueChangedInvoke> changedInvokes)
|
||||
@@ -108,7 +125,7 @@ namespace Robust.Shared.Configuration
|
||||
}
|
||||
catch
|
||||
{
|
||||
_sawmill.Error($"TOML parsed cvar does not match registered cvar type. Name: {cvar}. Code Type: {cfgVar.Type}. Toml type: {value.GetType()}");
|
||||
_sawmill.Error($"Parsed cvar does not match registered cvar type. Name: {cvar}. Code Type: {cfgVar.Type}. Parsed type: {value.GetType()}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -119,16 +136,24 @@ namespace Robust.Shared.Configuration
|
||||
else
|
||||
{
|
||||
//or add another unregistered CVar
|
||||
//Note: the initial defaultValue is null, but it will get overwritten when the cvar is registered.
|
||||
cfgVar = new ConfigVar(cvar, null!, CVar.NONE) { Value = value };
|
||||
_configVars.Add(cvar, cfgVar);
|
||||
cfgVar = AddUnregisteredCVar(cvar, value);
|
||||
}
|
||||
|
||||
cfgVar.ConfigModified = true;
|
||||
}
|
||||
|
||||
private ConfigVar AddUnregisteredCVar(string name, object value)
|
||||
{
|
||||
//Note: the initial defaultValue is null, but it will get overwritten when the cvar is registered.
|
||||
var cfgVar = new ConfigVar(name, null!, CVar.NONE) { Value = value };
|
||||
_configVars.Add(name, cfgVar);
|
||||
return cfgVar;
|
||||
}
|
||||
|
||||
public HashSet<string> LoadDefaultsFromTomlStream(Stream stream)
|
||||
{
|
||||
var tblRoot = Toml.ReadStream(stream);
|
||||
|
||||
var loaded = new HashSet<string>();
|
||||
|
||||
var callbackEvents = new ValueList<ValueChangedInvoke>();
|
||||
@@ -136,7 +161,7 @@ namespace Robust.Shared.Configuration
|
||||
// Ensure callbacks are raised OUTSIDE the write lock.
|
||||
using (Lock.WriteGuard())
|
||||
{
|
||||
foreach (var (cVarName, value) in ParseCVarValuesFromToml(stream))
|
||||
foreach (var (cVarName, value) in ParseCVarValuesFromToml(tblRoot))
|
||||
{
|
||||
if (!_configVars.TryGetValue(cVarName, out var cVar) || !cVar.Registered)
|
||||
{
|
||||
@@ -181,9 +206,13 @@ namespace Robust.Shared.Configuration
|
||||
{
|
||||
try
|
||||
{
|
||||
using var file = File.OpenRead(configFile);
|
||||
var result = LoadFromTomlStream(file);
|
||||
HashSet<string> result;
|
||||
using (var file = File.OpenRead(configFile))
|
||||
{
|
||||
result = LoadFromTomlStream(file);
|
||||
}
|
||||
SetSaveFile(configFile);
|
||||
ApplyRollback();
|
||||
_sawmill.Info($"Configuration loaded from file");
|
||||
return result;
|
||||
}
|
||||
@@ -223,6 +252,13 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveToTomlStream(Stream stream, IEnumerable<string> cvars)
|
||||
{
|
||||
var table = SaveToTomlTable(cvars);
|
||||
|
||||
Toml.WriteStream(table, stream);
|
||||
}
|
||||
|
||||
private TomlTable SaveToTomlTable(IEnumerable<string> cvars, Func<string, object>? overrideValue = null)
|
||||
{
|
||||
var tblRoot = Toml.Create();
|
||||
|
||||
@@ -233,10 +269,18 @@ namespace Robust.Shared.Configuration
|
||||
if (!_configVars.TryGetValue(name, out var cVar))
|
||||
continue;
|
||||
|
||||
var value = cVar.Value;
|
||||
if (value == null && cVar.Registered)
|
||||
object? value;
|
||||
if (overrideValue != null)
|
||||
{
|
||||
value = cVar.DefaultValue;
|
||||
value = overrideValue(name);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = cVar.Value;
|
||||
if (value == null && cVar.Registered)
|
||||
{
|
||||
value = cVar.DefaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
@@ -263,6 +307,8 @@ namespace Robust.Shared.Configuration
|
||||
}
|
||||
|
||||
//runtime unboxing, either this or generic hell... ¯\_(ツ)_/¯
|
||||
// If you add a type here, add it to .Rollback.cs too!!!
|
||||
// I can't share the code because of how the serialization layers work :(
|
||||
switch (value)
|
||||
{
|
||||
case Enum val:
|
||||
@@ -293,7 +339,7 @@ namespace Robust.Shared.Configuration
|
||||
}
|
||||
}
|
||||
|
||||
Toml.WriteStream(tblRoot, stream);
|
||||
return tblRoot;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -495,7 +541,7 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
public void LoadCVarsFromType(Type containingType)
|
||||
{
|
||||
foreach (var defField in containingType.GetFields(BindingFlags.Public | BindingFlags.Static))
|
||||
foreach (var defField in containingType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))
|
||||
{
|
||||
var fieldType = defField.FieldType;
|
||||
if (!fieldType.IsGenericType || fieldType.GetGenericTypeDefinition() != typeof(CVarDef<>))
|
||||
@@ -754,10 +800,25 @@ namespace Robust.Shared.Configuration
|
||||
|
||||
private void InvokeValueChanged(in ValueChangedInvoke invoke)
|
||||
{
|
||||
OnCVarValueChanged?.Invoke(invoke.Info);
|
||||
try
|
||||
{
|
||||
OnCVarValueChanged?.Invoke(invoke.Info);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Error while running OnCVarValueChanged callback: {e}");
|
||||
}
|
||||
|
||||
foreach (var entry in invoke.Invoke.Entries)
|
||||
{
|
||||
entry.Value!.Invoke(invoke.Value, in invoke.Info);
|
||||
try
|
||||
{
|
||||
entry.Value!.Invoke(invoke.Value, in invoke.Info);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Error while running OnValueChanged callback: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -768,10 +829,8 @@ namespace Robust.Shared.Configuration
|
||||
return new ValueChangedInvoke(info, var.ValueChanged);
|
||||
}
|
||||
|
||||
private IEnumerable<(string cvar, object value)> ParseCVarValuesFromToml(Stream stream)
|
||||
private IEnumerable<(string cvar, object value)> ParseCVarValuesFromToml(TomlTable tblRoot)
|
||||
{
|
||||
var tblRoot = Toml.ReadStream(stream);
|
||||
|
||||
return ProcessTomlObject(tblRoot, "");
|
||||
|
||||
IEnumerable<(string cvar, object value)> ProcessTomlObject(TomlObject obj, string tablePath)
|
||||
|
||||
@@ -272,5 +272,113 @@ namespace Robust.Shared.Configuration
|
||||
where T : notnull;
|
||||
|
||||
public event Action<CVarChangeInfo>? OnCVarValueChanged;
|
||||
|
||||
//
|
||||
// Rollback
|
||||
//
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot a CVar to be rolled back later, even in the event of a client crash.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This set of APIs is intended for settings menus that want to show the user a
|
||||
/// "Do these settings look correct?" prompt with timeout,
|
||||
/// so that settings can be rolled back even in the event of alt+F4 or client crash.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Rollback is applied on <see cref="ApplyRollback"/> call or client restart,
|
||||
/// unless CVars are unmarked again via <see cref="UnmarkForRollback(Robust.Shared.Configuration.CVarDef[])"/>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Rollback is tracked in the config file too, and this command does not save it automatically. Of course,
|
||||
/// not saving the config file before client exit also effectively rolls back CVars.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Calling this method if a CVar is already marked for rollback will simply update the snapshot value.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="cVars">The CVars to roll back.</param>
|
||||
/// <seealso cref="UnmarkForRollback(Robust.Shared.Configuration.CVarDef[])"/>
|
||||
void MarkForRollback(params CVarDef[] cVars);
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot a CVar to be rolled back later, even in the event of a client crash.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This set of APIs is intended for settings menus that want to show the user a
|
||||
/// "Do these settings look correct?" prompt with timeout,
|
||||
/// so that settings can be rolled back even in the event of alt+F4 or client crash.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Rollback is applied on <see cref="ApplyRollback"/> call or client restart,
|
||||
/// unless CVars are unmarked again via <see cref="UnmarkForRollback(string[])"/>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Rollback is tracked in the config file too, and this command does not save it automatically. Of course,
|
||||
/// not saving the config file before client exit also effectively rolls back CVars.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Calling this method if a CVar is already marked for rollback will simply update the snapshot value.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="cVars">The CVar names to snapshot and (possibly) roll back later.</param>
|
||||
/// <seealso cref="UnmarkForRollback(string[])"/>
|
||||
void MarkForRollback(params string[] cVars);
|
||||
|
||||
/// <summary>
|
||||
/// Unmark a CVar for rollback.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This set of APIs is intended for settings menus that want to show the user a
|
||||
/// "Do these settings look correct?" prompt with timeout,
|
||||
/// so that settings can be rolled back even in the event of alt+F4 or client crash.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Rollback is tracked in the config file too, and this command does not save it automatically.
|
||||
/// Users must still call <see cref="SaveToFile"/> manually to avoid rollback happening.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="cVars">The CVars to unmark for rollback.</param>
|
||||
/// <seealso cref="MarkForRollback(Robust.Shared.Configuration.CVarDef[])"/>
|
||||
/// <seealso cref="ApplyRollback"/>
|
||||
void UnmarkForRollback(params CVarDef[] cVars);
|
||||
|
||||
/// <summary>
|
||||
/// Unmark a CVar for rollback.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This set of APIs is intended for settings menus that want to show the user a
|
||||
/// "Do these settings look correct?" prompt with timeout,
|
||||
/// so that settings can be rolled back even in the event of alt+F4 or client crash.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Rollback is tracked in the config file too, and this command does not save it automatically.
|
||||
/// Users must still call <see cref="SaveToFile"/> manually to avoid rollback happening.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="cVars">The CVars to unmark for rollback.</param>
|
||||
/// <seealso cref="MarkForRollback(string[])"/>
|
||||
/// <seealso cref="ApplyRollback"/>
|
||||
void UnmarkForRollback(params string[] cVars);
|
||||
|
||||
/// <summary>
|
||||
/// Apply all pending CVar rollbacks.
|
||||
/// </summary>
|
||||
/// <para>
|
||||
/// This set of APIs is intended for settings menus that want to show the user a
|
||||
/// "Do these settings look correct?" prompt with timeout,
|
||||
/// so that settings can be rolled back even in the event of alt+F4 or client crash.
|
||||
/// </para>
|
||||
/// <remarks>
|
||||
/// This implicitly saves the config file to ensure the config file does not contain
|
||||
/// rollback data for longer than necessary.
|
||||
/// </remarks>
|
||||
/// <seealso cref="MarkForRollback(Robust.Shared.Configuration.CVarDef[])"/>
|
||||
/// <seealso cref="UnmarkForRollback(Robust.Shared.Configuration.CVarDef[])"/>
|
||||
void ApplyRollback();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Robust.Shared.ContentPack
|
||||
{
|
||||
internal sealed partial class AssemblyTypeChecker
|
||||
{
|
||||
public static IEnumerable<string> DumpMetaMembers(Type type)
|
||||
public static IEnumerable<(string Value, bool IsField)> DumpMetaMembers(Type type)
|
||||
{
|
||||
var assemblyLoc = type.Assembly.Location;
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace Robust.Shared.ContentPack
|
||||
var fieldName = metaReader.GetString(fieldDef.Name);
|
||||
var fieldType = fieldDef.DecodeSignature(provider, 0);
|
||||
|
||||
yield return $"{fieldType.WhitelistToString()} {fieldName}";
|
||||
yield return ($"{fieldType.WhitelistToString()} {fieldName}", IsField: true);
|
||||
}
|
||||
|
||||
foreach (var methodHandle in typeDef.GetMethods())
|
||||
@@ -79,7 +79,7 @@ namespace Robust.Shared.ContentPack
|
||||
? ""
|
||||
: $"<{new string(',', genericCount - 1)}>";
|
||||
|
||||
yield return $"{methodSig.ReturnType.WhitelistToString()} {methodName}{typeParamString}({paramString})";
|
||||
yield return ($"{methodSig.ReturnType.WhitelistToString()} {methodName}{typeParamString}({paramString})", IsField: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,13 @@ internal sealed record ResourceManifestData(
|
||||
string? DefaultWindowTitle,
|
||||
string? WindowIconSet,
|
||||
string? SplashLogo,
|
||||
bool? ShowLoadingBar,
|
||||
bool AutoConnect,
|
||||
string[]? ClientAssemblies
|
||||
)
|
||||
{
|
||||
public static readonly ResourceManifestData Default =
|
||||
new ResourceManifestData(Array.Empty<string>(), null, null, null, null, true, null);
|
||||
new ResourceManifestData(Array.Empty<string>(), null, null, null, null, null, true, null);
|
||||
|
||||
public static ResourceManifestData LoadResourceManifest(IResourceManager res)
|
||||
{
|
||||
@@ -58,6 +59,10 @@ internal sealed record ResourceManifestData(
|
||||
if (mapping.TryGetNode("splashLogo", out var splashNode))
|
||||
splashLogo = splashNode.AsString();
|
||||
|
||||
bool? showBar = null;
|
||||
if (mapping.TryGetNode("show_loading_bar", out var showBarNode))
|
||||
showBar = showBarNode.AsBool();
|
||||
|
||||
bool autoConnect = true;
|
||||
if (mapping.TryGetNode("autoConnect", out var autoConnectNode))
|
||||
autoConnect = autoConnectNode.AsBool();
|
||||
@@ -70,6 +75,7 @@ internal sealed record ResourceManifestData(
|
||||
defaultWindowTitle,
|
||||
windowIconSet,
|
||||
splashLogo,
|
||||
showBar,
|
||||
autoConnect,
|
||||
clientAssemblies
|
||||
);
|
||||
|
||||
@@ -309,6 +309,32 @@ Types:
|
||||
SixLabors.ImageSharp.Formats:
|
||||
IImageEncoder: { All: True }
|
||||
PixelTypeInfo: { All: True }
|
||||
SixLabors.ImageSharp.Processing:
|
||||
ResizeExtensions:
|
||||
Methods:
|
||||
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Size)"
|
||||
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Size, bool)"
|
||||
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int)"
|
||||
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, bool)"
|
||||
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler)"
|
||||
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Size, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler, bool)"
|
||||
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler, bool)"
|
||||
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler, SixLabors.ImageSharp.Rectangle, SixLabors.ImageSharp.Rectangle, bool)"
|
||||
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int, SixLabors.ImageSharp.Processing.Processors.Transforms.IResampler, SixLabors.ImageSharp.Rectangle, bool)"
|
||||
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Resize(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Processing.ResizeOptions)"
|
||||
ProcessingExtensions:
|
||||
Methods:
|
||||
- "SixLabors.ImageSharp.Image Clone(SixLabors.ImageSharp.Image, System.Action`1<SixLabors.ImageSharp.Processing.IImageProcessingContext>)"
|
||||
- "SixLabors.ImageSharp.Image Clone(SixLabors.ImageSharp.Image, SixLabors.ImageSharp.Configuration, System.Action`1<SixLabors.ImageSharp.Processing.IImageProcessingContext>)"
|
||||
- "SixLabors.ImageSharp.Image`1<!!0> Clone<>(SixLabors.ImageSharp.Image`1<!!0>, System.Action`1<SixLabors.ImageSharp.Processing.IImageProcessingContext>)"
|
||||
- "SixLabors.ImageSharp.Image`1<!!0> Clone<>(SixLabors.ImageSharp.Image`1<!!0>, SixLabors.ImageSharp.Configuration, System.Action`1<SixLabors.ImageSharp.Processing.IImageProcessingContext>)"
|
||||
- "SixLabors.ImageSharp.Image`1<!!0> Clone<>(SixLabors.ImageSharp.Image`1<!!0>, SixLabors.ImageSharp.Processing.Processors.IImageProcessor[])"
|
||||
- "SixLabors.ImageSharp.Image`1<!!0> Clone<>(SixLabors.ImageSharp.Image`1<!!0>, SixLabors.ImageSharp.Configuration, SixLabors.ImageSharp.Processing.Processors.IImageProcessor[])"
|
||||
CropExtensions:
|
||||
Methods:
|
||||
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Crop(SixLabors.ImageSharp.Processing.IImageProcessingContext, int, int)"
|
||||
- "SixLabors.ImageSharp.Processing.IImageProcessingContext Crop(SixLabors.ImageSharp.Processing.IImageProcessingContext, SixLabors.ImageSharp.Rectangle)"
|
||||
IImageProcessingContext: { }
|
||||
SixLabors.ImageSharp.PixelFormats:
|
||||
A8: { All: True }
|
||||
Argb32: { All: True }
|
||||
@@ -386,6 +412,7 @@ Types:
|
||||
- "void SaveAsPng(SixLabors.ImageSharp.Image, System.IO.Stream, SixLabors.ImageSharp.Formats.Png.PngEncoder)"
|
||||
- "void SaveAsTga(SixLabors.ImageSharp.Image, System.IO.Stream)"
|
||||
- "void SaveAsTga(SixLabors.ImageSharp.Image, System.IO.Stream, SixLabors.ImageSharp.Formats.Tga.TgaEncoder)"
|
||||
Rectangle: {All: True}
|
||||
Size: { All: True }
|
||||
SizeF: { All: True }
|
||||
System.Buffers:
|
||||
@@ -652,6 +679,8 @@ Types:
|
||||
MethodInfo: { }
|
||||
TypeAttributes: { } # Enum
|
||||
TypeInfo: { }
|
||||
System.Reflection.Metadata:
|
||||
MetadataUpdateHandlerAttribute: { All: True }
|
||||
System.Runtime.CompilerServices:
|
||||
AsyncStateMachineAttribute: { All: True }
|
||||
AsyncTaskMethodBuilder: { All: True }
|
||||
@@ -860,13 +889,19 @@ Types:
|
||||
- "System.Text.StringBuilder AppendFormat(System.IFormatProvider, string, object, object)"
|
||||
- "System.Text.StringBuilder AppendFormat(System.IFormatProvider, string, object, object, object)"
|
||||
- "System.Text.StringBuilder AppendFormat(System.IFormatProvider, string, object[])"
|
||||
- "System.Text.StringBuilder AppendJoin(char, System.ReadOnlySpan`1<object>)"
|
||||
- "System.Text.StringBuilder AppendJoin(char, System.ReadOnlySpan`1<string>)"
|
||||
- "System.Text.StringBuilder AppendJoin(char, object[])"
|
||||
- "System.Text.StringBuilder AppendJoin(char, string[])"
|
||||
- "System.Text.StringBuilder AppendJoin(string, System.ReadOnlySpan`1<object>)"
|
||||
- "System.Text.StringBuilder AppendJoin(string, System.ReadOnlySpan`1<string>)"
|
||||
- "System.Text.StringBuilder AppendJoin(string, object[])"
|
||||
- "System.Text.StringBuilder AppendJoin(string, string[])"
|
||||
- "System.Text.StringBuilder AppendJoin<>(char, System.Collections.Generic.IEnumerable`1<!!0>)"
|
||||
- "System.Text.StringBuilder AppendJoin<>(string, System.Collections.Generic.IEnumerable`1<!!0>)"
|
||||
- "System.Text.StringBuilder AppendLine()"
|
||||
- "System.Text.StringBuilder AppendLine(System.IFormatProvider, ref System.Text.StringBuilder/AppendInterpolatedStringHandler)"
|
||||
- "System.Text.StringBuilder AppendLine(ref System.Text.StringBuilder/AppendInterpolatedStringHandler)"
|
||||
- "System.Text.StringBuilder AppendLine(string)"
|
||||
- "System.Text.StringBuilder Clear()"
|
||||
- "System.Text.StringBuilder Insert(int, bool)"
|
||||
@@ -889,6 +924,8 @@ Types:
|
||||
- "System.Text.StringBuilder Insert(int, ulong)"
|
||||
- "System.Text.StringBuilder Insert(int, ushort)"
|
||||
- "System.Text.StringBuilder Remove(int, int)"
|
||||
- "System.Text.StringBuilder Replace(System.ReadOnlySpan`1<char>, System.ReadOnlySpan`1<char>)"
|
||||
- "System.Text.StringBuilder Replace(System.ReadOnlySpan`1<char>, System.ReadOnlySpan`1<char>, int, int)"
|
||||
- "System.Text.StringBuilder Replace(char, char)"
|
||||
- "System.Text.StringBuilder Replace(char, char, int, int)"
|
||||
- "System.Text.StringBuilder Replace(string, string)"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using Robust.Shared.Maths;
|
||||
@@ -33,7 +34,7 @@ public readonly struct CompIdx : IEquatable<CompIdx>
|
||||
var curLength = array.Length;
|
||||
if (curLength <= idx.Value)
|
||||
{
|
||||
var newLength = MathHelper.NextPowerOfTwo(Math.Max(8, idx.Value));
|
||||
var newLength = MathHelper.NextPowerOfTwo(Math.Max(8, idx.Value + 1));
|
||||
Array.Resize(ref array, newLength);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Robust.Shared.GameObjects;
|
||||
|
||||
internal sealed partial class EntityEventBus : IEventBus
|
||||
{
|
||||
private IEntityManager _entMan;
|
||||
private EntityManager _entMan;
|
||||
private IComponentFactory _comFac;
|
||||
private IReflectionManager _reflection;
|
||||
|
||||
@@ -34,18 +34,18 @@ internal sealed partial class EntityEventBus : IEventBus
|
||||
/// <summary>
|
||||
/// Array of component events and their handlers. The array is indexed by a component's
|
||||
/// <see cref="CompIdx.Value"/>, while the dictionary is indexed by the event type. This does not include events
|
||||
/// with the <see cref="ComponentEventAttribute"/>
|
||||
/// with the <see cref="ComponentEventAttribute"/>, unless <see cref="ComponentEventAttribute.Exclusive"/> is false.
|
||||
/// </summary>
|
||||
internal FrozenDictionary<Type, DirectedRegistration>[] _eventSubs = default!;
|
||||
private FrozenDictionary<Type, DirectedRegistration>[] _eventSubs = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Variant of <see cref="_eventSubs"/> that also includes events with the <see cref="ComponentEventAttribute"/>
|
||||
/// Variant of <see cref="_eventSubs"/> that only includes events with the <see cref="ComponentEventAttribute"/>
|
||||
/// </summary>
|
||||
internal FrozenDictionary<Type, DirectedRegistration>[] _compEventSubs = default!;
|
||||
private FrozenDictionary<Type, DirectedEventHandler>[] _compEventSubs = default!;
|
||||
|
||||
// pre-freeze event subscription data
|
||||
internal Dictionary<Type, DirectedRegistration>?[] _eventSubsUnfrozen =
|
||||
Array.Empty<Dictionary<Type, DirectedRegistration>>();
|
||||
private Dictionary<Type, DirectedRegistration>?[] _eventSubsUnfrozen = [];
|
||||
private Dictionary<Type, DirectedEventHandler>?[] _compEventSubsUnfrozen = [];
|
||||
|
||||
/// <summary>
|
||||
/// Inverse of <see cref="_eventSubs"/>, mapping event types to sets of components.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using Robust.Shared.Collections;
|
||||
@@ -101,9 +101,6 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
internal delegate void DirectedEventHandler(EntityUid uid, IComponent comp, ref Unit args);
|
||||
|
||||
private delegate void DirectedEventHandler<TEvent>(EntityUid uid, IComponent comp, ref TEvent args)
|
||||
where TEvent : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Max size of a components event subscription linked list.
|
||||
/// Used to limit the stackalloc in <see cref="EntDispatch"/>
|
||||
@@ -118,7 +115,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// </summary>
|
||||
/// <param name="entMan">The entity manager to watch for entity/component events.</param>
|
||||
/// <param name="reflection">The reflection manager to use when finding derived types.</param>
|
||||
public EntityEventBus(IEntityManager entMan, IReflectionManager reflection)
|
||||
public EntityEventBus(EntityManager entMan, IReflectionManager reflection)
|
||||
{
|
||||
_entMan = entMan;
|
||||
_comFac = entMan.ComponentFactory;
|
||||
@@ -176,13 +173,8 @@ namespace Robust.Shared.GameObjects
|
||||
public void RaiseComponentEvent<TEvent>(EntityUid uid, IComponent component, CompIdx type, ref TEvent args)
|
||||
where TEvent : notnull
|
||||
{
|
||||
ref var unitRef = ref Unsafe.As<TEvent, Unit>(ref args);
|
||||
|
||||
DispatchComponent<TEvent>(
|
||||
uid,
|
||||
component,
|
||||
type,
|
||||
ref unitRef);
|
||||
if (_compEventSubs[type.Value].TryGetValue(typeof(TEvent), out var handler))
|
||||
handler(uid, component, ref Unsafe.As<TEvent, Unit>(ref args));
|
||||
}
|
||||
|
||||
public void OnlyCallOnRobustUnitTestISwearToGodPleaseSomebodyKillThisNightmare()
|
||||
@@ -249,15 +241,13 @@ namespace Robust.Shared.GameObjects
|
||||
where TComp : IComponent
|
||||
where TEvent : notnull
|
||||
{
|
||||
void EventHandler(EntityUid uid, IComponent comp, ref TEvent args)
|
||||
=> handler(uid, (TComp)comp, args);
|
||||
void EventHandler(EntityUid uid, IComponent comp, ref Unit ev)
|
||||
{
|
||||
ref var tev = ref Unsafe.As<Unit, TEvent>(ref ev);
|
||||
handler(uid, (TComp) comp, tev);
|
||||
}
|
||||
|
||||
EntSubscribe<TEvent>(
|
||||
CompIdx.Index<TComp>(),
|
||||
typeof(TComp),
|
||||
typeof(TEvent),
|
||||
EventHandler,
|
||||
null);
|
||||
EntAddSubscription(CompIdx.Index<TComp>(), typeof(TComp), typeof(TEvent), EventHandler);
|
||||
}
|
||||
|
||||
public void SubscribeLocalEvent<TComp, TEvent>(
|
||||
@@ -268,71 +258,51 @@ namespace Robust.Shared.GameObjects
|
||||
where TComp : IComponent
|
||||
where TEvent : notnull
|
||||
{
|
||||
void EventHandler(EntityUid uid, IComponent comp, ref TEvent args)
|
||||
=> handler(uid, (TComp)comp, args);
|
||||
void EventHandler(EntityUid uid, IComponent comp, ref Unit ev)
|
||||
{
|
||||
ref var tev = ref Unsafe.As<Unit, TEvent>(ref ev);
|
||||
handler(uid, (TComp) comp, tev);
|
||||
}
|
||||
|
||||
var orderData = CreateOrderingData(orderType, before, after);
|
||||
|
||||
EntSubscribe<TEvent>(
|
||||
CompIdx.Index<TComp>(),
|
||||
typeof(TComp),
|
||||
typeof(TEvent),
|
||||
EventHandler,
|
||||
orderData);
|
||||
|
||||
RegisterCommon(typeof(TEvent), orderData, out _);
|
||||
EntAddSubscription(CompIdx.Index<TComp>(), typeof(TComp), typeof(TEvent), EventHandler, orderType, before, after);
|
||||
}
|
||||
|
||||
public void SubscribeLocalEvent<TComp, TEvent>(ComponentEventRefHandler<TComp, TEvent> handler)
|
||||
where TComp : IComponent where TEvent : notnull
|
||||
{
|
||||
void EventHandler(EntityUid uid, IComponent comp, ref TEvent args)
|
||||
=> handler(uid, (TComp)comp, ref args);
|
||||
void EventHandler(EntityUid uid, IComponent comp, ref Unit ev)
|
||||
{
|
||||
ref var tev = ref Unsafe.As<Unit, TEvent>(ref ev);
|
||||
handler(uid, (TComp) comp, ref tev);
|
||||
}
|
||||
|
||||
EntSubscribe<TEvent>(
|
||||
CompIdx.Index<TComp>(),
|
||||
typeof(TComp),
|
||||
typeof(TEvent),
|
||||
EventHandler,
|
||||
null);
|
||||
EntAddSubscription(CompIdx.Index<TComp>(), typeof(TComp), typeof(TEvent), EventHandler);
|
||||
}
|
||||
|
||||
public void SubscribeLocalEvent<TComp, TEvent>(ComponentEventRefHandler<TComp, TEvent> handler, Type orderType,
|
||||
Type[]? before = null,
|
||||
Type[]? after = null) where TComp : IComponent where TEvent : notnull
|
||||
{
|
||||
void EventHandler(EntityUid uid, IComponent comp, ref TEvent args)
|
||||
=> handler(uid, (TComp)comp, ref args);
|
||||
void EventHandler(EntityUid uid, IComponent comp, ref Unit ev)
|
||||
{
|
||||
ref var tev = ref Unsafe.As<Unit, TEvent>(ref ev);
|
||||
handler(uid, (TComp) comp, ref tev);
|
||||
}
|
||||
|
||||
var orderData = CreateOrderingData(orderType, before, after);
|
||||
|
||||
EntSubscribe<TEvent>(
|
||||
CompIdx.Index<TComp>(),
|
||||
typeof(TComp),
|
||||
typeof(TEvent),
|
||||
EventHandler,
|
||||
orderData);
|
||||
|
||||
RegisterCommon(typeof(TEvent), orderData, out _);
|
||||
EntAddSubscription(CompIdx.Index<TComp>(), typeof(TComp), typeof(TEvent), EventHandler, orderType, before, after);
|
||||
}
|
||||
|
||||
public void SubscribeLocalEvent<TComp, TEvent>(EntityEventRefHandler<TComp, TEvent> handler, Type orderType,
|
||||
Type[]? before = null,
|
||||
Type[]? after = null) where TComp : IComponent where TEvent : notnull
|
||||
{
|
||||
void EventHandler(EntityUid uid, IComponent comp, ref TEvent args)
|
||||
=> handler(new Entity<TComp>(uid, (TComp) comp), ref args);
|
||||
void EventHandler(EntityUid uid, IComponent comp, ref Unit ev)
|
||||
{
|
||||
ref var tev = ref Unsafe.As<Unit, TEvent>(ref ev);
|
||||
handler(new Entity<TComp>(uid, (TComp) comp), ref tev);
|
||||
}
|
||||
|
||||
var orderData = CreateOrderingData(orderType, before, after);
|
||||
|
||||
EntSubscribe<TEvent>(
|
||||
CompIdx.Index<TComp>(),
|
||||
typeof(TComp),
|
||||
typeof(TEvent),
|
||||
EventHandler,
|
||||
orderData);
|
||||
|
||||
RegisterCommon(typeof(TEvent), orderData, out _);
|
||||
EntAddSubscription(CompIdx.Index<TComp>(), typeof(TComp), typeof(TEvent), EventHandler, orderType, before, after);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -340,7 +310,24 @@ namespace Robust.Shared.GameObjects
|
||||
where TComp : IComponent
|
||||
where TEvent : notnull
|
||||
{
|
||||
EntUnsubscribe(CompIdx.Index<TComp>(), typeof(TEvent));
|
||||
if (!_comFac.TryGetRegistration(typeof(TComp), out _))
|
||||
{
|
||||
if (!IgnoreUnregisteredComponents)
|
||||
throw new InvalidOperationException($"Component is not a valid reference type: {typeof(TComp).Name}");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_subscriptionLock)
|
||||
throw new InvalidOperationException("Subscription locked.");
|
||||
|
||||
var i = CompIdx.ArrayIndex<TComp>();
|
||||
|
||||
_eventSubsUnfrozen[i]!.Remove(typeof(TEvent));
|
||||
_compEventSubsUnfrozen[i]!.Remove(typeof(TEvent));
|
||||
|
||||
if (_eventSubsInv.TryGetValue(typeof(TEvent), out var t))
|
||||
t.Remove(CompIdx.Index<TComp>());
|
||||
}
|
||||
|
||||
private void ComFacOnComponentsAdded(ComponentRegistration[] regs)
|
||||
@@ -351,6 +338,7 @@ namespace Robust.Shared.GameObjects
|
||||
foreach (var reg in regs)
|
||||
{
|
||||
CompIdx.RefArray(ref _eventSubsUnfrozen, reg.Idx) ??= new();
|
||||
CompIdx.RefArray(ref _compEventSubsUnfrozen, reg.Idx) ??= new();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,40 +362,17 @@ namespace Robust.Shared.GameObjects
|
||||
_subscriptionLock = true;
|
||||
_eventData = _eventDataUnfrozen.ToFrozenDictionary();
|
||||
|
||||
// Find last non-null entry.
|
||||
var last = 0;
|
||||
for (var i = 0; i < _eventSubsUnfrozen.Length; i++)
|
||||
{
|
||||
var entry = _eventSubsUnfrozen[i];
|
||||
if (entry != null)
|
||||
last = i;
|
||||
}
|
||||
|
||||
// TODO PERFORMANCE
|
||||
// make this only contain events that actually use comp-events
|
||||
// Assuming it makes the frozen dictionaries more specialized and thus faster.
|
||||
// AFAIK currently only MapInit is both a comp-event and a general event.
|
||||
// It should probably be changed to just be a comp event.
|
||||
_compEventSubs = _eventSubsUnfrozen
|
||||
.Take(last+1)
|
||||
_eventSubs = TrimNull(_eventSubsUnfrozen)
|
||||
.Select(dict => dict?.ToFrozenDictionary()!)
|
||||
.ToArray();
|
||||
|
||||
_eventSubs = _eventSubsUnfrozen
|
||||
.Take(last+1)
|
||||
.Select(dict => dict?.Where(x => !IsComponentEvent(x.Key)).ToFrozenDictionary()!)
|
||||
_compEventSubs = TrimNull(_compEventSubsUnfrozen)
|
||||
.Select(dict => dict?.ToFrozenDictionary()!)
|
||||
.ToArray();
|
||||
|
||||
CalcOrdering();
|
||||
}
|
||||
|
||||
private bool IsComponentEvent(Type t)
|
||||
{
|
||||
var isCompEv = _eventData[t].ComponentEvent;
|
||||
DebugTools.Assert(isCompEv == t.HasCustomAttribute<ComponentEventAttribute>());
|
||||
return isCompEv;
|
||||
}
|
||||
|
||||
public void OnComponentRemoved(in RemovedComponentEventArgs e)
|
||||
{
|
||||
EntRemoveComponent(e.BaseArgs.Owner, e.Idx);
|
||||
@@ -417,13 +382,15 @@ namespace Robust.Shared.GameObjects
|
||||
CompIdx compType,
|
||||
Type compTypeObj,
|
||||
Type eventType,
|
||||
DirectedRegistration registration)
|
||||
DirectedEventHandler handler,
|
||||
Type? orderType = null,
|
||||
Type[]? before = null,
|
||||
Type[]? after = null)
|
||||
{
|
||||
if (_subscriptionLock)
|
||||
throw new InvalidOperationException("Subscription locked.");
|
||||
|
||||
if (compType.Value >= _eventSubsUnfrozen.Length
|
||||
|| _eventSubsUnfrozen[compType.Value] is not { } compSubs)
|
||||
if (!_comFac.TryGetRegistration(compTypeObj, out _))
|
||||
{
|
||||
if (IgnoreUnregisteredComponents)
|
||||
return;
|
||||
@@ -431,53 +398,25 @@ namespace Robust.Shared.GameObjects
|
||||
throw new InvalidOperationException($"Component is not a valid reference type: {compTypeObj.Name}");
|
||||
}
|
||||
|
||||
if (compSubs.ContainsKey(eventType))
|
||||
if (eventType.GetCustomAttribute<ComponentEventAttribute>() is { } attr)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Duplicate Subscriptions for comp={compTypeObj}, event={eventType.Name}");
|
||||
}
|
||||
if (!_compEventSubsUnfrozen[compType.Value]!.TryAdd(eventType, handler))
|
||||
throw new InvalidOperationException($"Duplicate Subscriptions for comp={compTypeObj}, event={eventType.Name}");
|
||||
|
||||
compSubs.Add(eventType, registration);
|
||||
|
||||
RegisterCommon(eventType, registration.Ordering, out var data);
|
||||
data.ComponentEvent = eventType.HasCustomAttribute<ComponentEventAttribute>();
|
||||
if (!data.ComponentEvent)
|
||||
_eventSubsInv.GetOrNew(eventType).Add(compType);
|
||||
}
|
||||
|
||||
private void EntSubscribe<TEvent>(
|
||||
CompIdx compType,
|
||||
Type compTypeObj,
|
||||
Type eventType,
|
||||
DirectedEventHandler<TEvent> handler,
|
||||
OrderingData? order)
|
||||
where TEvent : notnull
|
||||
{
|
||||
EntAddSubscription(compType, compTypeObj, eventType, new DirectedRegistration(handler, order,
|
||||
(EntityUid uid, IComponent comp, ref Unit ev) =>
|
||||
{
|
||||
ref var tev = ref Unsafe.As<Unit, TEvent>(ref ev);
|
||||
handler(uid, comp, ref tev);
|
||||
}));
|
||||
}
|
||||
|
||||
private void EntUnsubscribe(CompIdx compType, Type eventType)
|
||||
{
|
||||
if (_subscriptionLock)
|
||||
throw new InvalidOperationException("Subscription locked.");
|
||||
|
||||
if (compType.Value >= _eventSubsUnfrozen.Length
|
||||
|| _eventSubsUnfrozen[compType.Value] is not { } compSubs)
|
||||
{
|
||||
if (IgnoreUnregisteredComponents)
|
||||
// An exclusive component-event is only raised via RaiseComponentEvent, hence it don't need a normal
|
||||
// directed event subscription
|
||||
if (attr.Exclusive)
|
||||
return;
|
||||
|
||||
throw new InvalidOperationException("Trying to unsubscribe from unregistered component!");
|
||||
}
|
||||
|
||||
var removed = compSubs.Remove(eventType);
|
||||
if (removed)
|
||||
_eventSubsInv[eventType].Remove(compType);
|
||||
var orderData = orderType == null ? null : CreateOrderingData(orderType, before, after);
|
||||
var reg = new DirectedRegistration(orderData, handler);
|
||||
|
||||
if (!_eventSubsUnfrozen[compType.Value]!.TryAdd(eventType, reg))
|
||||
throw new InvalidOperationException($"Duplicate Subscriptions for comp={compTypeObj}, event={eventType.Name}");
|
||||
|
||||
RegisterCommon(eventType, reg.Ordering, out _);
|
||||
_eventSubsInv.GetOrNew(eventType).Add(compType);
|
||||
}
|
||||
|
||||
private void EntAddEntity(EntityUid euid)
|
||||
@@ -501,8 +440,6 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
foreach (var evType in compSubs.Keys)
|
||||
{
|
||||
DebugTools.Assert(!_eventData[evType].ComponentEvent);
|
||||
|
||||
if (eventTable.Free < 0)
|
||||
GrowEventTable(eventTable);
|
||||
|
||||
@@ -563,7 +500,6 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
foreach (var evType in compSubs.Keys)
|
||||
{
|
||||
DebugTools.Assert(!_eventData[evType].ComponentEvent);
|
||||
ref var indices = ref CollectionsMarshal.GetValueRefOrNullRef(eventTable.EventIndices, evType);
|
||||
if (Unsafe.IsNullRef(ref indices))
|
||||
{
|
||||
@@ -667,17 +603,6 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
private void DispatchComponent<TEvent>(
|
||||
EntityUid euid,
|
||||
IComponent component,
|
||||
CompIdx baseType,
|
||||
ref Unit args)
|
||||
where TEvent : notnull
|
||||
{
|
||||
if (_compEventSubs[baseType.Value].TryGetValue(typeof(TEvent), out var reg))
|
||||
reg.Handler(euid, component, ref args);
|
||||
}
|
||||
|
||||
public void ClearSubscriptions()
|
||||
{
|
||||
_subscriptionLock = false;
|
||||
@@ -691,6 +616,10 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
sub?.Clear();
|
||||
}
|
||||
foreach (var sub in _compEventSubsUnfrozen)
|
||||
{
|
||||
sub?.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -705,22 +634,14 @@ namespace Robust.Shared.GameObjects
|
||||
_compEventSubs = null!;
|
||||
_eventSubs = null!;
|
||||
_eventSubsUnfrozen = null!;
|
||||
_compEventSubsUnfrozen = null!;
|
||||
_eventSubsInv = null!;
|
||||
}
|
||||
|
||||
internal sealed class DirectedRegistration : OrderedRegistration
|
||||
internal sealed class DirectedRegistration(OrderingData? ordering, DirectedEventHandler handler)
|
||||
: OrderedRegistration(ordering)
|
||||
{
|
||||
public readonly Delegate Original;
|
||||
public readonly DirectedEventHandler Handler;
|
||||
|
||||
public DirectedRegistration(
|
||||
Delegate original,
|
||||
OrderingData? ordering,
|
||||
DirectedEventHandler handler) : base(ordering)
|
||||
{
|
||||
Original = original;
|
||||
Handler = handler;
|
||||
}
|
||||
public readonly DirectedEventHandler Handler = handler;
|
||||
|
||||
public void SetOrder(int order)
|
||||
{
|
||||
@@ -753,6 +674,48 @@ namespace Robust.Shared.GameObjects
|
||||
public int Next;
|
||||
public CompIdx Component;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a new array with any trailing null entries removed.
|
||||
/// </summary>
|
||||
public static T[] TrimNull<T>(T[] input)
|
||||
{
|
||||
// Find last non-null entry.
|
||||
var last = 0;
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var entry = input[i];
|
||||
if (entry != null)
|
||||
last = i;
|
||||
}
|
||||
|
||||
return input[..(last + 1)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an array of event handlers for a given component event, indexed by the component's net-id.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For most events, this will generally be a pretty sparse array, with most entries being null. However, for
|
||||
/// the get and handle state events, this array will be relatively dense and helps save PVS a lot of save a
|
||||
/// FrozenDictionary lookups.
|
||||
/// </remarks>
|
||||
internal DirectedEventHandler?[] GetNetCompEventHandlers<TEvent>()
|
||||
{
|
||||
DebugTools.Assert(_subscriptionLock);
|
||||
DebugTools.Assert(typeof(TEvent).HasCustomAttribute<ComponentEventAttribute>());
|
||||
|
||||
var netComps = _comFac.NetworkedComponents!;
|
||||
var result = new DirectedEventHandler?[netComps.Count];
|
||||
|
||||
for (var i = 0; i < netComps.Count; i++)
|
||||
{
|
||||
var reg = netComps[i];
|
||||
result[i] = _compEventSubs[reg.Idx.Value].GetValueOrDefault(typeof(TEvent));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/// <seealso cref="ComponentEventRefHandler{TComp, TEvent}"/>
|
||||
|
||||
@@ -406,7 +406,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
var eventArgs = new AddedComponentEventArgs(new ComponentEventArgs(component, uid), reg);
|
||||
ComponentAdded?.Invoke(eventArgs);
|
||||
_eventBus.OnComponentAdded(eventArgs);
|
||||
EventBusInternal.OnComponentAdded(eventArgs);
|
||||
|
||||
LifeAddToEntity(uid, component, reg.Idx);
|
||||
|
||||
@@ -427,7 +427,7 @@ namespace Robust.Shared.GameObjects
|
||||
LifeStartup(uid, component, reg.Idx);
|
||||
|
||||
if (metadata.EntityLifeStage >= EntityLifeStage.MapInitialized)
|
||||
EventBus.RaiseComponentEvent(uid, component, reg.Idx, MapInitEventInstance);
|
||||
EventBusInternal.RaiseComponentEvent(uid, component, reg.Idx, MapInitEventInstance);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -717,7 +717,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
var eventArgs = new RemovedComponentEventArgs(new ComponentEventArgs(component, entityUid), false, metadata, idx);
|
||||
ComponentRemoved?.Invoke(eventArgs);
|
||||
_eventBus.OnComponentRemoved(eventArgs);
|
||||
EventBusInternal.OnComponentRemoved(eventArgs);
|
||||
|
||||
if (!terminating)
|
||||
{
|
||||
@@ -1708,9 +1708,12 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
|
||||
public bool CanGetComponentState(IEventBus eventBus, IComponent component, ICommonSession player)
|
||||
=> CanGetComponentState(component, player);
|
||||
|
||||
public bool CanGetComponentState(IComponent component, ICommonSession player)
|
||||
{
|
||||
var attempt = new ComponentGetStateAttemptEvent(player);
|
||||
eventBus.RaiseComponentEvent(component.Owner, component, ref attempt);
|
||||
EventBusInternal.RaiseComponentEvent(component.Owner, component, ref attempt);
|
||||
return !attempt.Cancelled;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ public partial class EntityManager
|
||||
component.CreationTick = CurrentTick;
|
||||
// networked components are assumed to be dirty when added to entities. See also: ClearTicks()
|
||||
component.LastModifiedTick = CurrentTick;
|
||||
EventBus.RaiseComponentEvent(uid, component, idx, CompAddInstance);
|
||||
EventBusInternal.RaiseComponentEvent(uid, component, idx, CompAddInstance);
|
||||
component.LifeStage = ComponentLifeStage.Added;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
@@ -39,7 +39,7 @@ public partial class EntityManager
|
||||
DebugTools.Assert(component.LifeStage == ComponentLifeStage.Added);
|
||||
|
||||
component.LifeStage = ComponentLifeStage.Initializing;
|
||||
EventBus.RaiseComponentEvent(uid, component, idx, CompInitInstance);
|
||||
EventBusInternal.RaiseComponentEvent(uid, component, idx, CompInitInstance);
|
||||
component.LifeStage = ComponentLifeStage.Initialized;
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public partial class EntityManager
|
||||
DebugTools.Assert(component.LifeStage == ComponentLifeStage.Initialized);
|
||||
|
||||
component.LifeStage = ComponentLifeStage.Starting;
|
||||
EventBus.RaiseComponentEvent(uid, component, idx, CompStartupInstance);
|
||||
EventBusInternal.RaiseComponentEvent(uid, component, idx, CompStartupInstance);
|
||||
component.LifeStage = ComponentLifeStage.Running;
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ public partial class EntityManager
|
||||
}
|
||||
|
||||
component.LifeStage = ComponentLifeStage.Stopping;
|
||||
EventBus.RaiseComponentEvent(uid, component, idx, CompShutdownInstance);
|
||||
EventBusInternal.RaiseComponentEvent(uid, component, idx, CompShutdownInstance);
|
||||
component.LifeStage = ComponentLifeStage.Stopped;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ public partial class EntityManager
|
||||
DebugTools.Assert(component.LifeStage != ComponentLifeStage.PreAdd);
|
||||
|
||||
component.LifeStage = ComponentLifeStage.Removing;
|
||||
EventBus.RaiseComponentEvent(uid, component, idx, CompRemoveInstance);
|
||||
EventBusInternal.RaiseComponentEvent(uid, component, idx, CompRemoveInstance);
|
||||
component.LifeStage = ComponentLifeStage.Deleted;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,14 +81,14 @@ namespace Robust.Shared.GameObjects
|
||||
/// </summary>
|
||||
protected readonly HashSet<EntityUid> Entities = new();
|
||||
|
||||
private EntityEventBus _eventBus = null!;
|
||||
internal EntityEventBus EventBusInternal = null!;
|
||||
|
||||
protected int NextEntityUid = (int) EntityUid.FirstUid;
|
||||
|
||||
protected int NextNetworkId = (int) NetEntity.First;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEventBus EventBus => _eventBus;
|
||||
public IEventBus EventBus => EventBusInternal;
|
||||
|
||||
public event Action<Entity<MetaDataComponent>>? EntityAdded;
|
||||
public event Action<Entity<MetaDataComponent>>? EntityInitialized;
|
||||
@@ -142,7 +142,7 @@ namespace Robust.Shared.GameObjects
|
||||
if (Initialized)
|
||||
throw new InvalidOperationException("Initialize() called multiple times");
|
||||
|
||||
_eventBus = new EntityEventBus(this, _reflection);
|
||||
EventBusInternal = new EntityEventBus(this, _reflection);
|
||||
|
||||
InitializeComponents();
|
||||
_metaReg = _componentFactory.GetRegistration(typeof(MetaDataComponent));
|
||||
@@ -210,6 +210,9 @@ namespace Robust.Shared.GameObjects
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Failed to serialize {compName} component of entity prototype {prototype.ID}. Exception: {e.Message}");
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -230,7 +233,7 @@ namespace Robust.Shared.GameObjects
|
||||
// TODO: Probably better to call this on its own given it's so infrequent.
|
||||
_entitySystemManager.Initialize();
|
||||
Started = true;
|
||||
_eventBus.LockSubscriptions();
|
||||
EventBusInternal.LockSubscriptions();
|
||||
_mapSystem = System<SharedMapSystem>();
|
||||
_xforms = System<SharedTransformSystem>();
|
||||
_containers = System<SharedContainerSystem>();
|
||||
@@ -245,7 +248,7 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
ShuttingDown = true;
|
||||
FlushEntities();
|
||||
_eventBus.ClearSubscriptions();
|
||||
EventBusInternal.ClearSubscriptions();
|
||||
_entitySystemManager.Shutdown();
|
||||
ClearComponents();
|
||||
ShuttingDown = false;
|
||||
@@ -259,8 +262,8 @@ namespace Robust.Shared.GameObjects
|
||||
ShuttingDown = true;
|
||||
FlushEntities();
|
||||
_entitySystemManager.Clear();
|
||||
_eventBus.Dispose();
|
||||
_eventBus = null!;
|
||||
EventBusInternal.Dispose();
|
||||
EventBusInternal = null!;
|
||||
ClearComponents();
|
||||
|
||||
ShuttingDown = false;
|
||||
@@ -279,18 +282,13 @@ namespace Robust.Shared.GameObjects
|
||||
using (histogram?.WithLabels("EntityEventBus").NewTimer())
|
||||
using (_prof.Group("Events"))
|
||||
{
|
||||
_eventBus.ProcessEventQueue();
|
||||
EventBusInternal.ProcessEventQueue();
|
||||
}
|
||||
|
||||
using (histogram?.WithLabels("QueuedDeletion").NewTimer())
|
||||
using (_prof.Group("QueueDel"))
|
||||
{
|
||||
while (QueuedDeletions.TryDequeue(out var uid))
|
||||
{
|
||||
DeleteEntity(uid);
|
||||
}
|
||||
|
||||
QueuedDeletionsSet.Clear();
|
||||
ProcessQueueudDeletions();
|
||||
}
|
||||
|
||||
using (histogram?.WithLabels("ComponentCull").NewTimer())
|
||||
@@ -300,6 +298,16 @@ namespace Robust.Shared.GameObjects
|
||||
}
|
||||
}
|
||||
|
||||
internal virtual void ProcessQueueudDeletions()
|
||||
{
|
||||
while (QueuedDeletions.TryDequeue(out var uid))
|
||||
{
|
||||
DeleteEntity(uid);
|
||||
}
|
||||
|
||||
QueuedDeletionsSet.Clear();
|
||||
}
|
||||
|
||||
public virtual void FrameUpdate(float frameTime)
|
||||
{
|
||||
_entitySystemManager.FrameUpdate(frameTime);
|
||||
@@ -596,11 +604,14 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
var ev = new EntityTerminatingEvent((uid, metadata));
|
||||
BeforeEntityTerminating?.Invoke(ref ev);
|
||||
EventBus.RaiseLocalEvent(uid, ref ev, true);
|
||||
EventBusInternal.RaiseLocalEvent(uid, ref ev, true);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Caught exception while raising event {nameof(EntityTerminatingEvent)} on entity {ToPrettyString(uid, metadata)}\n{e}");
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
|
||||
foreach (var child in xform._children)
|
||||
@@ -643,6 +654,9 @@ namespace Robust.Shared.GameObjects
|
||||
catch(Exception e)
|
||||
{
|
||||
_sawmill.Error($"Caught exception while trying to recursively delete child entity '{ToPrettyString(child)}' of '{ToPrettyString(uid, metadata)}'\n{e}");
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -661,6 +675,9 @@ namespace Robust.Shared.GameObjects
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Caught exception while trying to call shutdown on component of entity '{ToPrettyString(uid, metadata)}'\n{e}");
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -676,9 +693,12 @@ namespace Robust.Shared.GameObjects
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Caught exception while invoking event {nameof(EntityDeleted)} on '{ToPrettyString(uid, metadata)}'\n{e}");
|
||||
#if !EXCEPTION_TOLERANCE
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
|
||||
_eventBus.OnEntityDeleted(uid);
|
||||
EventBusInternal.OnEntityDeleted(uid);
|
||||
Entities.Remove(uid);
|
||||
// Need to get the ID above before MetadataComponent shutdown but only remove it after everything else is done.
|
||||
NetEntityLookup.Remove(metadata.NetEntity);
|
||||
@@ -912,7 +932,7 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
// we want this called before adding components
|
||||
EntityAdded?.Invoke((uid, metadata));
|
||||
_eventBus.OnEntityAdded(uid);
|
||||
EventBusInternal.OnEntityAdded(uid);
|
||||
|
||||
Entities.Add(uid);
|
||||
// add the required MetaDataComponent directly.
|
||||
@@ -1029,7 +1049,7 @@ namespace Robust.Shared.GameObjects
|
||||
DebugTools.Assert(meta.EntityLifeStage == EntityLifeStage.Initialized, $"Expected entity {ToPrettyString(entity)} to be initialized, was {meta.EntityLifeStage}");
|
||||
SetLifeStage(meta, EntityLifeStage.MapInitialized);
|
||||
|
||||
EventBus.RaiseLocalEvent(entity, MapInitEventInstance);
|
||||
EventBusInternal.RaiseLocalEvent(entity, MapInitEventInstance);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -148,7 +148,7 @@ namespace Robust.Shared.GameObjects
|
||||
{
|
||||
foreach (var sub in _subscriptions)
|
||||
{
|
||||
sub.Unsubscribe(this, EntityManager.EventBus);
|
||||
sub.Unsubscribe(this, EntityManager.EventBusInternal);
|
||||
}
|
||||
|
||||
_subscriptions = default;
|
||||
|
||||
@@ -99,22 +99,22 @@ namespace Robust.Shared.GameObjects
|
||||
|
||||
protected void RaiseLocalEvent<T>(T message) where T : notnull
|
||||
{
|
||||
EntityManager.EventBus.RaiseEvent(EventSource.Local, message);
|
||||
EntityManager.EventBusInternal.RaiseEvent(EventSource.Local, message);
|
||||
}
|
||||
|
||||
protected void RaiseLocalEvent<T>(ref T message) where T : notnull
|
||||
{
|
||||
EntityManager.EventBus.RaiseEvent(EventSource.Local, ref message);
|
||||
EntityManager.EventBusInternal.RaiseEvent(EventSource.Local, ref message);
|
||||
}
|
||||
|
||||
protected void RaiseLocalEvent(object message)
|
||||
{
|
||||
EntityManager.EventBus.RaiseEvent(EventSource.Local, message);
|
||||
EntityManager.EventBusInternal.RaiseEvent(EventSource.Local, message);
|
||||
}
|
||||
|
||||
protected void QueueLocalEvent(EntityEventArgs message)
|
||||
{
|
||||
EntityManager.EventBus.QueueEvent(EventSource.Local, message);
|
||||
EntityManager.EventBusInternal.QueueEvent(EventSource.Local, message);
|
||||
}
|
||||
|
||||
protected void RaiseNetworkEvent(EntityEventArgs message)
|
||||
@@ -158,23 +158,36 @@ namespace Robust.Shared.GameObjects
|
||||
protected void RaiseLocalEvent<TEvent>(EntityUid uid, TEvent args, bool broadcast = false)
|
||||
where TEvent : notnull
|
||||
{
|
||||
EntityManager.EventBus.RaiseLocalEvent(uid, args, broadcast);
|
||||
EntityManager.EventBusInternal.RaiseLocalEvent(uid, args, broadcast);
|
||||
}
|
||||
|
||||
protected void RaiseLocalEvent(EntityUid uid, object args, bool broadcast = false)
|
||||
{
|
||||
EntityManager.EventBus.RaiseLocalEvent(uid, args, broadcast);
|
||||
EntityManager.EventBusInternal.RaiseLocalEvent(uid, args, broadcast);
|
||||
}
|
||||
|
||||
protected void RaiseLocalEvent<TEvent>(EntityUid uid, ref TEvent args, bool broadcast = false)
|
||||
where TEvent : notnull
|
||||
{
|
||||
EntityManager.EventBus.RaiseLocalEvent(uid, ref args, broadcast);
|
||||
EntityManager.EventBusInternal.RaiseLocalEvent(uid, ref args, broadcast);
|
||||
}
|
||||
|
||||
protected void RaiseLocalEvent(EntityUid uid, ref object args, bool broadcast = false)
|
||||
{
|
||||
EntityManager.EventBus.RaiseLocalEvent(uid, ref args, broadcast);
|
||||
EntityManager.EventBusInternal.RaiseLocalEvent(uid, ref args, broadcast);
|
||||
}
|
||||
|
||||
protected void RaiseComponentEvent<TEvent, TComp>(EntityUid uid, TComp comp, ref TEvent args)
|
||||
where TEvent : notnull
|
||||
where TComp : IComponent
|
||||
{
|
||||
EntityManager.EventBusInternal.RaiseComponentEvent(uid, comp, ref args);
|
||||
}
|
||||
|
||||
public void RaiseComponentEvent<TEvent>(EntityUid uid, IComponent component, ref TEvent args)
|
||||
where TEvent : notnull
|
||||
{
|
||||
EntityManager.EventBusInternal.RaiseComponentEvent(uid, component, ref args);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -3,15 +3,17 @@ using System;
|
||||
namespace Robust.Shared.GameObjects;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
|
||||
public sealed class ByRefEventAttribute : Attribute
|
||||
{
|
||||
}
|
||||
public sealed class ByRefEventAttribute : Attribute;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that an eventbus event should only ever be raised through <see cref="IDirectedEventBus.RaiseComponentEvent{TEvent}(IComponent, TEvent)"/>.
|
||||
/// This allows extra optimizations.
|
||||
/// This attribute enables an event to be raised as a "component event" via <see cref="IDirectedEventBus.RaiseComponentEvent{TEvent}(EntityUid, IComponent, TEvent)"/>.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
|
||||
internal sealed class ComponentEventAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// If true, this event may **only** be raised via as a "component event", as any subscriptions will not be
|
||||
/// included in the normal event tables.
|
||||
/// </summary>
|
||||
public bool Exclusive = true;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ namespace Robust.Shared.GameObjects
|
||||
/// <summary>
|
||||
/// Raised directed on an entity when the map is initialized.
|
||||
/// </summary>
|
||||
[ComponentEvent(Exclusive = false)]
|
||||
public sealed class MapInitEvent : EntityEventArgs
|
||||
{
|
||||
}
|
||||
|
||||
@@ -405,9 +405,8 @@ public sealed partial class EntityLookupSystem
|
||||
EntityUid? ignored = null)
|
||||
{
|
||||
var broadphaseInv = _transform.GetInvWorldMatrix(lookupUid);
|
||||
var polygon = new SlimPolygon(worldBounds, broadphaseInv, out var localAABB);
|
||||
|
||||
var localAABB = broadphaseInv.TransformBox(worldBounds);
|
||||
var polygon = new SlimPolygon(localAABB);
|
||||
var result = AnyEntitiesIntersecting(lookupUid,
|
||||
polygon,
|
||||
localAABB,
|
||||
@@ -768,9 +767,7 @@ public sealed partial class EntityLookupSystem
|
||||
if (!_broadQuery.TryGetComponent(gridId, out var lookup))
|
||||
return;
|
||||
|
||||
var localAABB = _transform.GetInvWorldMatrix(gridId).TransformBox(worldBounds);
|
||||
var polygon = new SlimPolygon(localAABB);
|
||||
|
||||
var polygon = new SlimPolygon(worldBounds, _transform.GetInvWorldMatrix(gridId), out var localAABB);
|
||||
AddEntitiesIntersecting(gridId, intersecting, polygon, localAABB, Physics.Transform.Empty, flags, lookup);
|
||||
AddContained(intersecting, flags);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Robust.Shared.Map;
|
||||
@@ -132,6 +133,7 @@ public abstract partial class SharedTransformSystem
|
||||
/// <summary>
|
||||
/// Creates map-relative <see cref="EntityCoordinates"/> given some <see cref="MapCoordinates"/>.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public EntityCoordinates ToCoordinates(MapCoordinates coordinates)
|
||||
{
|
||||
if (_map.TryGetMap(coordinates.MapId, out var uid))
|
||||
@@ -145,11 +147,13 @@ public abstract partial class SharedTransformSystem
|
||||
/// <summary>
|
||||
/// Returns the grid that the entity whose position the coordinates are relative to is on.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public EntityUid? GetGrid(EntityCoordinates coordinates)
|
||||
{
|
||||
return GetGrid(coordinates.EntityId);
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public EntityUid? GetGrid(Entity<TransformComponent?> entity)
|
||||
{
|
||||
return !Resolve(entity, ref entity.Comp, logMissing:false) ? null : entity.Comp.GridUid;
|
||||
@@ -158,6 +162,7 @@ public abstract partial class SharedTransformSystem
|
||||
/// <summary>
|
||||
/// Returns the Map Id these coordinates are on.
|
||||
/// </summary>
|
||||
[Pure]
|
||||
public MapId GetMapId(EntityCoordinates coordinates)
|
||||
{
|
||||
return GetMapId(coordinates.EntityId);
|
||||
|
||||
@@ -50,6 +50,7 @@ namespace Robust.Shared.IoC
|
||||
/// <remarks>
|
||||
/// This property will be null if <see cref="InitThread()"/> has not been called on this thread yet.
|
||||
/// </remarks>
|
||||
[Obsolete("Resolve the IDependencyCollection directly without static methods (e.g., with a [Dependency] IDependencyCollection field)")]
|
||||
public static IDependencyCollection? Instance => _container.IsValueCreated ? _container.Value : null;
|
||||
|
||||
/// <summary>
|
||||
@@ -102,6 +103,7 @@ namespace Robust.Shared.IoC
|
||||
/// Thrown if <paramref name="overwrite"/> is false and <typeparamref name="TInterface"/> has been registered before,
|
||||
/// or if an already instantiated interface (by <see cref="BuildGraph"/>) is attempting to be overwritten.
|
||||
/// </exception>
|
||||
[Obsolete("Use an IDependencyCollection instance instead of static methods")]
|
||||
public static void Register<TInterface, [MeansImplicitUse] TImplementation>(bool overwrite = false)
|
||||
where TImplementation : class, TInterface
|
||||
where TInterface : class
|
||||
@@ -123,6 +125,7 @@ namespace Robust.Shared.IoC
|
||||
/// Thrown if <paramref name="overwrite"/> is false and <typeparamref name="T"/> has been registered before,
|
||||
/// or if an already instantiated interface (by <see cref="BuildGraph"/>) is attempting to be overwritten.
|
||||
/// </exception>
|
||||
[Obsolete("Use an IDependencyCollection instance instead of static methods")]
|
||||
public static void Register<[MeansImplicitUse] T>(bool overwrite = false) where T : class
|
||||
{
|
||||
Register<T, T>(overwrite);
|
||||
@@ -143,6 +146,7 @@ namespace Robust.Shared.IoC
|
||||
/// Thrown if <paramref name="overwrite"/> is false and <typeparamref name="TInterface"/> has been registered before,
|
||||
/// or if an already instantiated interface (by <see cref="BuildGraph"/>) is attempting to be overwritten.
|
||||
/// </exception>
|
||||
[Obsolete("Use an IDependencyCollection instance instead of static methods")]
|
||||
public static void Register<TInterface, TImplementation>(DependencyFactoryDelegate<TImplementation> factory, bool overwrite = false)
|
||||
where TImplementation : class, TInterface
|
||||
where TInterface : class
|
||||
@@ -165,6 +169,7 @@ namespace Robust.Shared.IoC
|
||||
/// If true, do not throw an <see cref="InvalidOperationException"/> if an interface is already registered,
|
||||
/// replace the current implementation instead.
|
||||
/// </param>
|
||||
[Obsolete("Use an IDependencyCollection instance instead of static methods")]
|
||||
public static void RegisterInstance<TInterface>(object implementation, bool overwrite = false)
|
||||
where TInterface : class
|
||||
{
|
||||
@@ -193,6 +198,7 @@ namespace Robust.Shared.IoC
|
||||
/// because the object graph still needs to be constructed for it.
|
||||
/// </exception>
|
||||
[System.Diagnostics.Contracts.Pure]
|
||||
[Obsolete("Use dependency injection or an IDependencyCollection instance instead of static methods")]
|
||||
public static T Resolve<T>()
|
||||
{
|
||||
DebugTools.Assert(_container.IsValueCreated, NoContextAssert);
|
||||
@@ -202,6 +208,7 @@ namespace Robust.Shared.IoC
|
||||
|
||||
/// <inheritdoc cref="Resolve{T}()"/>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Obsolete("Use dependency injection or an IDependencyCollection instance instead of static methods")]
|
||||
public static void Resolve<T>([NotNull] ref T? instance)
|
||||
{
|
||||
// Do not call into IDependencyCollection immediately for this,
|
||||
@@ -214,6 +221,7 @@ namespace Robust.Shared.IoC
|
||||
/// Resolve two dependencies manually.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Obsolete("Use dependency injection or an IDependencyCollection instance instead of static methods")]
|
||||
public static void Resolve<T1, T2>([NotNull] ref T1? instance1, [NotNull] ref T2? instance2)
|
||||
{
|
||||
DebugTools.Assert(_container.IsValueCreated, NoContextAssert);
|
||||
@@ -226,6 +234,7 @@ namespace Robust.Shared.IoC
|
||||
/// Resolve three dependencies manually.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Obsolete("Use dependency injection or an IDependencyCollection instance instead of static methods")]
|
||||
public static void Resolve<T1, T2, T3>([NotNull] ref T1? instance1, [NotNull] ref T2? instance2, [NotNull] ref T3? instance3)
|
||||
{
|
||||
DebugTools.Assert(_container.IsValueCreated, NoContextAssert);
|
||||
@@ -238,6 +247,7 @@ namespace Robust.Shared.IoC
|
||||
/// Resolve four dependencies manually.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[Obsolete("Use dependency injection or an IDependencyCollection instance instead of static methods")]
|
||||
public static void Resolve<T1, T2, T3, T4>([NotNull] ref T1? instance1, [NotNull] ref T2? instance2, [NotNull] ref T3? instance3, [NotNull] ref T4? instance4)
|
||||
{
|
||||
DebugTools.Assert(_container.IsValueCreated, NoContextAssert);
|
||||
@@ -254,6 +264,7 @@ namespace Robust.Shared.IoC
|
||||
/// because the object graph still needs to be constructed for it.
|
||||
/// </exception>
|
||||
[System.Diagnostics.Contracts.Pure]
|
||||
[Obsolete("Use dependency injection or an IDependencyCollection instance instead of static methods")]
|
||||
public static object ResolveType(Type type)
|
||||
{
|
||||
DebugTools.Assert(_container.IsValueCreated, NoContextAssert);
|
||||
@@ -284,6 +295,7 @@ namespace Robust.Shared.IoC
|
||||
/// Thrown if a dependency field on the object is not registered.
|
||||
/// </exception>
|
||||
/// <seealso cref="BuildGraph"/>
|
||||
[Obsolete("Use an IDependencyCollection instance instead of static methods")]
|
||||
public static T InjectDependencies<T>(T obj) where T : notnull
|
||||
{
|
||||
DebugTools.Assert(_container.IsValueCreated, NoContextAssert);
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Robust.Shared.Log
|
||||
/// The instance we're using.
|
||||
/// As it's a direct proxy to IoC this will not work if IoC is not functional.
|
||||
/// </summary>
|
||||
// TODO: Maybe cache this to improve performance.
|
||||
// TODO: Kill
|
||||
private static ILogManager LogManagerSingleton => IoCManager.Resolve<ILogManager>();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -241,27 +241,7 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
|
||||
}
|
||||
|
||||
IsTouching = touching;
|
||||
var status = ContactStatus.NoContact;
|
||||
|
||||
if (!wasTouching)
|
||||
{
|
||||
if (touching)
|
||||
{
|
||||
status = ContactStatus.StartTouching;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!touching)
|
||||
{
|
||||
status = ContactStatus.EndTouching;
|
||||
}
|
||||
// Still touching
|
||||
else
|
||||
{
|
||||
status = ContactStatus.Touching;
|
||||
}
|
||||
}
|
||||
var status = GetContactStatus(this, wasTouching);
|
||||
|
||||
#if DEBUG
|
||||
if (!sensor)
|
||||
@@ -273,6 +253,32 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
|
||||
return status;
|
||||
}
|
||||
|
||||
[Pure]
|
||||
internal static ContactStatus GetContactStatus(Contact contact, bool wasTouching)
|
||||
{
|
||||
if (!wasTouching)
|
||||
{
|
||||
if (contact.IsTouching)
|
||||
{
|
||||
return ContactStatus.StartTouching;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!contact.IsTouching)
|
||||
{
|
||||
return ContactStatus.EndTouching;
|
||||
}
|
||||
// Still touching
|
||||
else
|
||||
{
|
||||
return ContactStatus.Touching;
|
||||
}
|
||||
}
|
||||
|
||||
return ContactStatus.NoContact;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trimmed down version of <see cref="Update"/> that only updates whether or not the contact's shapes are
|
||||
/// touching.
|
||||
|
||||
@@ -41,6 +41,8 @@ internal record struct Polygon : IPhysShape
|
||||
|
||||
}
|
||||
|
||||
public Polygon(PhysShapeAabb aabb) : this(aabb.LocalBounds) {}
|
||||
|
||||
public Polygon(PolygonShape polyShape)
|
||||
{
|
||||
Unsafe.SkipInit(out this);
|
||||
@@ -52,6 +54,24 @@ internal record struct Polygon : IPhysShape
|
||||
polyShape.Normals.AsSpan()[..VertexCount].CopyTo(_normals.AsSpan);
|
||||
}
|
||||
|
||||
internal Polygon(SlimPolygon slim)
|
||||
{
|
||||
Unsafe.SkipInit(out this);
|
||||
Radius = slim.Radius;
|
||||
VertexCount = slim.VertexCount;
|
||||
|
||||
_vertices._00 = slim._vertices._00;
|
||||
_vertices._01 = slim._vertices._01;
|
||||
_vertices._02 = slim._vertices._02;
|
||||
_vertices._03 = slim._vertices._03;
|
||||
|
||||
_normals._00 = slim._normals._00;
|
||||
_normals._01 = slim._normals._01;
|
||||
_normals._02 = slim._normals._02;
|
||||
_normals._03 = slim._normals._03;
|
||||
Centroid = slim.Centroid;
|
||||
}
|
||||
|
||||
public Polygon(Box2 box)
|
||||
{
|
||||
Unsafe.SkipInit(out this);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
@@ -33,7 +36,6 @@ internal record struct SlimPolygon : IPhysShape
|
||||
|
||||
public SlimPolygon(Box2 box)
|
||||
{
|
||||
Unsafe.SkipInit(out this);
|
||||
Radius = 0f;
|
||||
|
||||
_vertices._00 = box.BottomLeft;
|
||||
@@ -45,27 +47,74 @@ internal record struct SlimPolygon : IPhysShape
|
||||
_normals._01 = new Vector2(1.0f, 0.0f);
|
||||
_normals._02 = new Vector2(0.0f, 1.0f);
|
||||
_normals._03 = new Vector2(-1.0f, 0.0f);
|
||||
Centroid = box.Center;
|
||||
}
|
||||
|
||||
public SlimPolygon(Box2Rotated bounds)
|
||||
public SlimPolygon(in Box2 box, in Matrix3x2 transform, out Box2 aabb)
|
||||
{
|
||||
Unsafe.SkipInit(out this);
|
||||
transform.TransformBox(box, out var x, out var y);
|
||||
var tmp = SimdHelpers.GetAABB(x, y);
|
||||
aabb = Unsafe.As<Vector128<float>, Box2>(ref tmp);
|
||||
|
||||
if (Sse.IsSupported)
|
||||
{
|
||||
var span = MemoryMarshal.Cast<Vector2, Vector128<float>>(_vertices.AsSpan);
|
||||
span[0] = Sse.UnpackLow(x, y);
|
||||
span[1] = Sse.UnpackHigh(x, y);
|
||||
}
|
||||
else
|
||||
{
|
||||
_vertices._00 = new Vector2(x[0], y[0]);
|
||||
_vertices._01 = new Vector2(x[1], y[1]);
|
||||
_vertices._02 = new Vector2(x[2], y[2]);
|
||||
_vertices._03 = new Vector2(x[3], y[3]);
|
||||
}
|
||||
|
||||
Radius = 0f;
|
||||
Centroid = (_vertices._00 + _vertices._02) / 2;
|
||||
|
||||
_vertices._00 = bounds.BottomLeft;
|
||||
_vertices._01 = bounds.BottomRight;
|
||||
_vertices._02 = bounds.TopRight;
|
||||
_vertices._03 = bounds.TopLeft;
|
||||
|
||||
// TODO SIMD
|
||||
// Probably use a special case for SlimPolygon
|
||||
Polygon.CalculateNormals(_vertices.AsSpan, _normals.AsSpan, 4);
|
||||
|
||||
Centroid = bounds.Center;
|
||||
}
|
||||
|
||||
public Box2 ComputeAABB(Transform transform, int childIndex)
|
||||
public SlimPolygon(in Box2Rotated box, in Matrix3x2 transform, out Box2 aabb)
|
||||
: this(in box.Box, box.Transform * transform, out aabb)
|
||||
{
|
||||
}
|
||||
|
||||
public SlimPolygon(in Box2Rotated box)
|
||||
{
|
||||
Unsafe.SkipInit(out this);
|
||||
box.GetVertices(out var x, out var y);
|
||||
|
||||
if (Sse.IsSupported)
|
||||
{
|
||||
var span = MemoryMarshal.Cast<Vector2, Vector128<float>>(_vertices.AsSpan);
|
||||
span[0] = Sse.UnpackLow(x, y);
|
||||
span[1] = Sse.UnpackHigh(x, y);
|
||||
}
|
||||
else
|
||||
{
|
||||
_vertices._00 = new Vector2(x[0], y[0]);
|
||||
_vertices._01 = new Vector2(x[1], y[1]);
|
||||
_vertices._02 = new Vector2(x[2], y[2]);
|
||||
_vertices._03 = new Vector2(x[3], y[3]);
|
||||
}
|
||||
|
||||
Radius = 0f;
|
||||
Centroid = (_vertices._00 + _vertices._02) / 2;
|
||||
|
||||
// TODO SIMD
|
||||
// Probably use a special case for SlimPolygon
|
||||
Polygon.CalculateNormals(_vertices.AsSpan, _normals.AsSpan, 4);
|
||||
}
|
||||
|
||||
public Box2 ComputeAABBSlow(Transform transform)
|
||||
{
|
||||
// This is just Polygon.ComputeAABB
|
||||
DebugTools.Assert(VertexCount > 0);
|
||||
DebugTools.Assert(childIndex == 0);
|
||||
var verts = _vertices.AsSpan;
|
||||
var lower = Transform.Mul(transform, verts[0]);
|
||||
var upper = lower;
|
||||
@@ -81,6 +130,36 @@ internal record struct SlimPolygon : IPhysShape
|
||||
return new Box2(lower - r, upper + r);
|
||||
}
|
||||
|
||||
public Box2 ComputeAABBSse(Transform transform)
|
||||
{
|
||||
var span = MemoryMarshal.Cast<Vector2, Vector128<float>>(_vertices.AsSpan);
|
||||
// Span = [x0, y0, x1, y1], [x2, y2, x3, y3]
|
||||
|
||||
var polyX = Sse.Shuffle(span[0], span[1], 0b_10_00_10_00);
|
||||
var polyY = Sse.Shuffle(span[0], span[1], 0b_11_01_11_01);
|
||||
// polyX = [x0, x1, x2, x3], polyY = [y0, y1, y2, y3]
|
||||
|
||||
Transform.MulSimd(transform, polyX, polyY, out var x, out var y);
|
||||
var lbrt = SimdHelpers.GetAABB(x, y);
|
||||
|
||||
// Next we enlarge the bounds by th radius. i.e, box.Enlarged(R);
|
||||
// TODO is this even needed for SlimPoly? Is the radius ever set to non-zero?
|
||||
var zero = Vector128<float>.Zero;
|
||||
var r = Vector128.Create(Radius);
|
||||
lbrt = lbrt - Sse.MoveLowToHigh(r, zero) + Sse.MoveHighToLow(r, zero);
|
||||
// lbrt = lbrt - [R, R, 0, 0] + [0, 0, R, R]
|
||||
|
||||
return Unsafe.As<Vector128<float>, Box2>(ref lbrt);
|
||||
}
|
||||
|
||||
public Box2 ComputeAABB(Transform transform, int childIndex)
|
||||
{
|
||||
DebugTools.Assert(childIndex == 0);
|
||||
return Sse.IsSupported
|
||||
? ComputeAABBSse(transform)
|
||||
: ComputeAABBSlow(transform);
|
||||
}
|
||||
|
||||
public bool Equals(SlimPolygon other)
|
||||
{
|
||||
return Radius.Equals(other.Radius) && _vertices.AsSpan[..VertexCount].SequenceEqual(other._vertices.AsSpan[..VertexCount]);
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace Robust.Shared.Physics.Systems
|
||||
}
|
||||
}
|
||||
|
||||
public static MassData GetMassData(IPhysShape shape, float density)
|
||||
public static MassData GetMassData<T>(T shape, float density) where T : IPhysShape
|
||||
{
|
||||
var data = new MassData();
|
||||
|
||||
@@ -82,7 +82,7 @@ namespace Robust.Shared.Physics.Systems
|
||||
return data;
|
||||
}
|
||||
|
||||
public static void GetMassData(IPhysShape shape, ref MassData data, float density)
|
||||
public static void GetMassData<T>(T shape, ref MassData data, float density) where T : IPhysShape
|
||||
{
|
||||
// Box2D just calls fixture.GetMassData which just calls the shape method anyway soooo
|
||||
// we can just cut out the middle-man
|
||||
@@ -106,16 +106,17 @@ namespace Robust.Shared.Physics.Systems
|
||||
data.I = data.Mass * (0.5f * circle.Radius * circle.Radius + Vector2.Dot(circle.Position, circle.Position));
|
||||
break;
|
||||
case PhysShapeAabb aabb:
|
||||
var polygon = (PolygonShape) aabb;
|
||||
var polygon = new Polygon(aabb);
|
||||
GetMassData(polygon, ref data, density);
|
||||
break;
|
||||
case Polygon fastPoly:
|
||||
GetMassData(new PolygonShape(fastPoly), ref data, density);
|
||||
case PolygonShape fatPoly:
|
||||
GetMassData(new Polygon(fatPoly), ref data, density);
|
||||
break;
|
||||
case SlimPolygon slim:
|
||||
GetMassData(new PolygonShape(slim), ref data, density);
|
||||
var slimPoly = new Polygon(slim);
|
||||
GetMassData(slimPoly, ref data, density);
|
||||
break;
|
||||
case PolygonShape poly:
|
||||
case Polygon poly:
|
||||
// Polygon mass, centroid, and inertia.
|
||||
// Let rho be the polygon density in mass per unit area.
|
||||
// Then:
|
||||
@@ -142,6 +143,7 @@ namespace Robust.Shared.Physics.Systems
|
||||
|
||||
var count = poly.VertexCount;
|
||||
DebugTools.Assert(count >= 3);
|
||||
DebugTools.Assert(poly._normals._00 != Vector2.Zero);
|
||||
|
||||
Vector2 center = new(0.0f, 0.0f);
|
||||
float area = 0.0f;
|
||||
@@ -149,15 +151,16 @@ namespace Robust.Shared.Physics.Systems
|
||||
|
||||
// Get a reference point for forming triangles.
|
||||
// Use the first vertex to reduce round-off errors.
|
||||
var s = poly.Vertices[0];
|
||||
var s = poly._vertices._00;
|
||||
var polySpan = poly._vertices.AsSpan;
|
||||
|
||||
const float k_inv3 = 1.0f / 3.0f;
|
||||
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
// Triangle vertices.
|
||||
var e1 = poly.Vertices[i] - s;
|
||||
var e2 = i + 1 < count ? poly.Vertices[i+1] - s : poly.Vertices[0] - s;
|
||||
var e1 = polySpan[i] - s;
|
||||
var e2 = i + 1 < count ? polySpan[i+1] - s : polySpan[0] - s;
|
||||
|
||||
float D = Vector2Helpers.Cross(e1, e2);
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -35,10 +32,9 @@ namespace Robust.Shared.Physics.Systems
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
|
||||
private float _broadphaseExpand;
|
||||
private readonly HashSet<FixtureProxy> _gridMoveBuffer = new();
|
||||
|
||||
private readonly Dictionary<EntityUid, Matrix3x2> _broadMatrices = new();
|
||||
private HashSet<FixtureProxy> _gridMoveBuffer = new();
|
||||
private float _frameTime;
|
||||
|
||||
/*
|
||||
* Okay so Box2D has its own "MoveProxy" stuff so you can easily find new contacts when required.
|
||||
@@ -55,9 +51,9 @@ namespace Robust.Shared.Physics.Systems
|
||||
|
||||
_contactJob = new()
|
||||
{
|
||||
_mapManager = _mapManager,
|
||||
MapManager = _mapManager,
|
||||
System = this,
|
||||
BroadphaseExpand = _broadphaseExpand,
|
||||
TransformSys = EntityManager.System<SharedTransformSystem>(),
|
||||
// TODO: EntityManager one isn't ready yet?
|
||||
XformQuery = GetEntityQuery<TransformComponent>(),
|
||||
};
|
||||
@@ -71,13 +67,13 @@ namespace Robust.Shared.Physics.Systems
|
||||
UpdatesOutsidePrediction = true;
|
||||
UpdatesAfter.Add(typeof(SharedTransformSystem));
|
||||
|
||||
Subs.CVar(_cfg, CVars.BroadphaseExpand, SetBroadphaseExpand, true);
|
||||
}
|
||||
|
||||
private void SetBroadphaseExpand(float value)
|
||||
{
|
||||
_contactJob.BroadphaseExpand = value;
|
||||
_broadphaseExpand = value;
|
||||
Subs.CVar(_cfg,
|
||||
CVars.TargetMinimumTickrate,
|
||||
val =>
|
||||
{
|
||||
_frameTime = 1f / val;
|
||||
},
|
||||
true);
|
||||
}
|
||||
|
||||
public void Rebuild(BroadphaseComponent component, bool fullBuild)
|
||||
@@ -109,6 +105,7 @@ namespace Robust.Shared.Physics.Systems
|
||||
// This is so that if we're on a broadphase that's moving (e.g. a grid) we need to make sure anything
|
||||
// we move over is getting checked for collisions, and putting it on the movebuffer is the easiest way to do so.
|
||||
var moveBuffer = _physicsSystem.MoveBuffer;
|
||||
_gridMoveBuffer.Clear();
|
||||
|
||||
foreach (var gridUid in movedGrids)
|
||||
{
|
||||
@@ -120,7 +117,7 @@ namespace Robust.Shared.Physics.Systems
|
||||
continue;
|
||||
|
||||
var worldAABB = _transform.GetWorldMatrix(xform).TransformBox(grid.LocalAABB);
|
||||
var enlargedAABB = worldAABB.Enlarged(_broadphaseExpand);
|
||||
var enlargedAABB = worldAABB.Enlarged(GetBroadphaseExpand(_physicsQuery.GetComponent(gridUid), _frameTime));
|
||||
var state = (moveBuffer, _gridMoveBuffer);
|
||||
|
||||
QueryMapBroadphase(mapBroadphase.DynamicTree, ref state, enlargedAABB);
|
||||
@@ -135,6 +132,11 @@ namespace Robust.Shared.Physics.Systems
|
||||
}
|
||||
}
|
||||
|
||||
private float GetBroadphaseExpand(PhysicsComponent body, float frameTime)
|
||||
{
|
||||
return body.LinearVelocity.Length() * 1.2f * frameTime;
|
||||
}
|
||||
|
||||
private void QueryMapBroadphase(IBroadPhase broadPhase,
|
||||
ref (HashSet<FixtureProxy>, HashSet<FixtureProxy>) state,
|
||||
Box2 enlargedAABB)
|
||||
@@ -163,11 +165,12 @@ namespace Robust.Shared.Physics.Systems
|
||||
/// </summary>
|
||||
internal void FindNewContacts()
|
||||
{
|
||||
_contactJob.FrameTime = _frameTime;
|
||||
_contactJob.Pairs.Clear();
|
||||
|
||||
var moveBuffer = _physicsSystem.MoveBuffer;
|
||||
var movedGrids = _physicsSystem.MovedGrids;
|
||||
|
||||
_gridMoveBuffer.Clear();
|
||||
|
||||
// Find any entities being driven over that might need to be considered
|
||||
FindGridContacts(movedGrids);
|
||||
|
||||
@@ -195,55 +198,32 @@ namespace Robust.Shared.Physics.Systems
|
||||
_contactJob.MoveBuffer.Add(proxy);
|
||||
}
|
||||
|
||||
_broadMatrices.Clear();
|
||||
var broadQuery = AllEntityQuery<BroadphaseComponent>();
|
||||
|
||||
// Cache broadphase matrices up front.
|
||||
// We'll defer the proxy world AABBs until we get contacts rather than doing it on every single move.
|
||||
// This is because contacts are run in parallel so we can spread the work a bit more and also don't duplicate it per tick.
|
||||
while (broadQuery.MoveNext(out var bUid, out _))
|
||||
{
|
||||
_broadMatrices[bUid] = _transform.GetWorldMatrix(bUid);
|
||||
}
|
||||
|
||||
for (var i = _contactJob.ContactBuffer.Count; i < _contactJob.MoveBuffer.Count; i++)
|
||||
{
|
||||
_contactJob.ContactBuffer.Add(new List<FixtureProxy>());
|
||||
}
|
||||
|
||||
var count = moveBuffer.Count;
|
||||
|
||||
_parallel.ProcessNow(_contactJob, count);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
foreach (var (proxyA, proxyB, flags) in _contactJob.Pairs)
|
||||
{
|
||||
var proxies = _contactJob.ContactBuffer[i];
|
||||
var otherBody = proxyB.Body;
|
||||
var contactFlags = ContactFlags.None;
|
||||
|
||||
if (proxies.Count == 0)
|
||||
continue;
|
||||
|
||||
var proxyA = _contactJob.MoveBuffer[i];
|
||||
var proxyABody = proxyA.Body;
|
||||
|
||||
_fixturesQuery.TryGetComponent(proxyA.Entity, out var manager);
|
||||
|
||||
foreach (var other in proxies)
|
||||
// Because we may be colliding with something asleep (due to the way grid movement works) need
|
||||
// to make sure the contact doesn't fail.
|
||||
// This is because we generate a contact across 2 different broadphases where both bodies aren't
|
||||
// moving locally but are moving in world-terms.
|
||||
if ((flags & PairFlag.Wake) == PairFlag.Wake)
|
||||
{
|
||||
var otherBody = other.Body;
|
||||
|
||||
// Because we may be colliding with something asleep (due to the way grid movement works) need
|
||||
// to make sure the contact doesn't fail.
|
||||
// This is because we generate a contact across 2 different broadphases where both bodies aren't
|
||||
// moving locally but are moving in world-terms.
|
||||
if (proxyA.Fixture.Hard && other.Fixture.Hard &&
|
||||
(_gridMoveBuffer.Contains(proxyA) || _gridMoveBuffer.Contains(other)))
|
||||
{
|
||||
_physicsSystem.WakeBody(proxyA.Entity, force: true, manager: manager, body: proxyABody);
|
||||
_physicsSystem.WakeBody(other.Entity, force: true, body: otherBody);
|
||||
}
|
||||
|
||||
_physicsSystem.AddPair(proxyA.FixtureId, other.FixtureId, proxyA, other);
|
||||
_physicsSystem.WakeBody(proxyA.Entity, force: true, body: proxyA.Body);
|
||||
_physicsSystem.WakeBody(proxyB.Entity, force: true, body: otherBody);
|
||||
}
|
||||
|
||||
// TODO: Actually implement this for grids, atm they have their own skrungly fixture handling which prevents this.
|
||||
if ((PairFlag.Grid & flags) == PairFlag.Grid)
|
||||
{
|
||||
contactFlags |= ContactFlags.Grid;
|
||||
}
|
||||
|
||||
_physicsSystem.AddPair(proxyA.FixtureId, proxyB.FixtureId, proxyA, proxyB, flags: contactFlags);
|
||||
}
|
||||
|
||||
moveBuffer.Clear();
|
||||
@@ -252,6 +232,8 @@ namespace Robust.Shared.Physics.Systems
|
||||
|
||||
private void HandleGridCollisions(HashSet<EntityUid> movedGrids)
|
||||
{
|
||||
// TODO: Could move this into its own job.
|
||||
// Ideally we'd just have some way to flag an entity as "AABB moves not proxy" into its own movebuffer.
|
||||
foreach (var gridUid in movedGrids)
|
||||
{
|
||||
var grid = _gridQuery.GetComponent(gridUid);
|
||||
@@ -301,6 +283,12 @@ namespace Robust.Shared.Physics.Systems
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the other entity is lower ID and also moved then let that handle the collision.
|
||||
if (tuple.grid.Owner.Id > uid.Id && tuple._physicsSystem.MovedGrids.Contains(uid))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var (_, _, otherGridMatrix, otherGridInvMatrix) = tuple.xformSystem.GetWorldPositionRotationMatrixWithInv(collidingXform);
|
||||
var otherGridBounds = otherGridMatrix.TransformBox(component.LocalAABB);
|
||||
var otherTransform = tuple._physicsSystem.GetPhysicsTransform(uid);
|
||||
@@ -337,6 +325,10 @@ namespace Robust.Shared.Physics.Systems
|
||||
{
|
||||
var otherFixture = fixturesB.Fixtures[otherId];
|
||||
|
||||
// There's already a contact so ignore it.
|
||||
if (fixture.Contacts.ContainsKey(otherFixture))
|
||||
break;
|
||||
|
||||
for (var j = 0; j < otherFixture.Shape.ChildCount; j++)
|
||||
{
|
||||
var otherAABB = otherFixture.Shape.ComputeAABB(otherTransform, j);
|
||||
@@ -370,7 +362,7 @@ namespace Robust.Shared.Physics.Systems
|
||||
FixtureProxy proxy,
|
||||
Box2 worldAABB,
|
||||
EntityUid broadphase,
|
||||
List<FixtureProxy> pairBuffer)
|
||||
List<(FixtureProxy, FixtureProxy, PairFlag)> pairBuffer)
|
||||
{
|
||||
DebugTools.Assert(proxy.Body.CanCollide);
|
||||
|
||||
@@ -401,7 +393,7 @@ namespace Robust.Shared.Physics.Systems
|
||||
}
|
||||
|
||||
var broadphaseComp = _broadphaseQuery.GetComponent(broadphase);
|
||||
var state = (pairBuffer, proxy);
|
||||
var state = (pairBuffer, _physicsSystem.MoveBuffer, this, _physicsSystem, proxy);
|
||||
|
||||
QueryBroadphase(broadphaseComp.DynamicTree, state, aabb);
|
||||
|
||||
@@ -411,23 +403,57 @@ namespace Robust.Shared.Physics.Systems
|
||||
QueryBroadphase(broadphaseComp.StaticTree, state, aabb);
|
||||
}
|
||||
|
||||
private void QueryBroadphase(IBroadPhase broadPhase, (List<FixtureProxy>, FixtureProxy) state, Box2 aabb)
|
||||
private void QueryBroadphase(IBroadPhase broadPhase, (List<(FixtureProxy, FixtureProxy, PairFlag)>, HashSet<FixtureProxy> MoveBuffer, SharedBroadphaseSystem Broadphase, SharedPhysicsSystem PhysicsSystem, FixtureProxy) state, Box2 aabb)
|
||||
{
|
||||
broadPhase.QueryAabb(ref state, static (
|
||||
ref (List<FixtureProxy> pairBuffer, FixtureProxy proxy) tuple,
|
||||
ref (List<(FixtureProxy, FixtureProxy, PairFlag)> pairs, HashSet<FixtureProxy> moveBuffer, SharedBroadphaseSystem broadphase, SharedPhysicsSystem physicsSystem, FixtureProxy proxy) tuple,
|
||||
in FixtureProxy other) =>
|
||||
{
|
||||
DebugTools.Assert(other.Body.CanCollide);
|
||||
// Logger.DebugS("physics", $"Checking {proxy.Entity} against {other.Fixture.Body.Owner} at {aabb}");
|
||||
|
||||
if (tuple.proxy == other ||
|
||||
!SharedPhysicsSystem.ShouldCollide(tuple.proxy.Fixture, other.Fixture) ||
|
||||
tuple.proxy.Entity == other.Entity)
|
||||
if (tuple.proxy.Entity == other.Entity ||
|
||||
!SharedPhysicsSystem.ShouldCollide(tuple.proxy.Fixture, other.Fixture))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
tuple.pairBuffer.Add(other);
|
||||
// Avoid creating duplicate pairs.
|
||||
// We give priority to whoever has the lower entity ID.
|
||||
if (tuple.proxy.Entity.Id > other.Entity.Id)
|
||||
{
|
||||
// Let the other fixture handle it.
|
||||
if (tuple.moveBuffer.Contains(other))
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if contact already exists.
|
||||
if (tuple.proxy.Fixture.Contacts.ContainsKey(other.Fixture))
|
||||
return true;
|
||||
|
||||
// TODO: Add in the slow path check here but turnstiles currently explodes this on content so.
|
||||
if (!tuple.physicsSystem.ShouldCollideJoints(tuple.proxy.Entity, other.Entity))
|
||||
return true;
|
||||
|
||||
// TODO: Sensors handled elsewhere when we do v3 port.
|
||||
//if (!tuple.proxy.Fixture.Hard || !other.Fixture.Hard)
|
||||
// return true;
|
||||
|
||||
// TODO: Check if interlocked + array is better here which is what box2d does
|
||||
// It then just heap allocates anything over the array size.
|
||||
var flags = PairFlag.None;
|
||||
if (tuple.proxy.Fixture.Hard &&
|
||||
other.Fixture.Hard &&
|
||||
(tuple.broadphase._gridMoveBuffer.Contains(tuple.proxy) || tuple.broadphase._gridMoveBuffer.Contains(other)))
|
||||
{
|
||||
flags |= PairFlag.Wake;
|
||||
}
|
||||
|
||||
lock (tuple.pairs)
|
||||
{
|
||||
tuple.pairs.Add((tuple.proxy, other, flags));
|
||||
}
|
||||
|
||||
return true;
|
||||
}, aabb, true);
|
||||
}
|
||||
@@ -560,39 +586,42 @@ namespace Robust.Shared.Physics.Systems
|
||||
{
|
||||
public SharedBroadphaseSystem System = default!;
|
||||
public SharedTransformSystem TransformSys = default!;
|
||||
public IMapManager _mapManager = default!;
|
||||
|
||||
public float BroadphaseExpand;
|
||||
public IMapManager MapManager = default!;
|
||||
|
||||
public EntityQuery<TransformComponent> XformQuery;
|
||||
|
||||
public List<List<FixtureProxy>> ContactBuffer = new();
|
||||
public List<FixtureProxy> MoveBuffer = new();
|
||||
public readonly List<FixtureProxy> MoveBuffer = new();
|
||||
|
||||
public int BatchSize => 8;
|
||||
public List<(FixtureProxy, FixtureProxy, PairFlag)> Pairs = new(64);
|
||||
|
||||
public float FrameTime;
|
||||
|
||||
// Box2D uses 64 but we have to do grid queries for each fixtureproxy which will add a fair bit of overhead.
|
||||
// Plus we also run events + trycomp for joints on top.
|
||||
public int BatchSize => 16;
|
||||
|
||||
public void Execute(int index)
|
||||
{
|
||||
var proxy = MoveBuffer[index];
|
||||
var broadphaseUid = XformQuery.GetComponent(proxy.Entity).Broadphase?.Uid;
|
||||
var worldAABB = System._broadMatrices[broadphaseUid!.Value].TransformBox(proxy.AABB);
|
||||
var buffer = ContactBuffer[index];
|
||||
buffer.Clear();
|
||||
var worldAABB = TransformSys.GetWorldMatrix(broadphaseUid!.Value).TransformBox(proxy.AABB);
|
||||
|
||||
var mapUid = XformQuery.GetComponent(proxy.Entity).MapUid ?? EntityUid.Invalid;
|
||||
|
||||
var broadphaseExpand = System.GetBroadphaseExpand(proxy.Body, FrameTime);
|
||||
|
||||
var proxyBody = proxy.Body;
|
||||
DebugTools.Assert(!proxyBody.Deleted);
|
||||
|
||||
var state = (System, proxy, worldAABB, buffer);
|
||||
var state = (System, proxy, worldAABB, Pairs);
|
||||
|
||||
// Get every broadphase we may be intersecting.
|
||||
_mapManager.FindGridsIntersecting(mapUid, worldAABB.Enlarged(BroadphaseExpand), ref state,
|
||||
MapManager.FindGridsIntersecting(mapUid, worldAABB.Enlarged(broadphaseExpand), ref state,
|
||||
static (EntityUid uid, MapGridComponent _, ref (
|
||||
SharedBroadphaseSystem system,
|
||||
FixtureProxy proxy,
|
||||
Box2 worldAABB,
|
||||
List<FixtureProxy> pairBuffer) tuple) =>
|
||||
List<(FixtureProxy, FixtureProxy, PairFlag)> pairBuffer) tuple) =>
|
||||
{
|
||||
ref var buffer = ref tuple.pairBuffer;
|
||||
tuple.system.FindPairs(tuple.proxy, tuple.worldAABB, uid, buffer);
|
||||
@@ -602,9 +631,24 @@ namespace Robust.Shared.Physics.Systems
|
||||
includeMap: false);
|
||||
|
||||
// Struct ref moment, I have no idea what's fastest.
|
||||
buffer = state.buffer;
|
||||
System.FindPairs(proxy, worldAABB, mapUid, buffer);
|
||||
System.FindPairs(proxy, worldAABB, mapUid, Pairs);
|
||||
}
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum PairFlag : byte
|
||||
{
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Should we wake the contacting entities.
|
||||
/// </summary>
|
||||
Wake = 1 << 0,
|
||||
|
||||
/// <summary>
|
||||
/// Is it a grid collision.
|
||||
/// </summary>
|
||||
Grid = 1 << 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,16 +263,13 @@ public abstract partial class SharedPhysicsSystem
|
||||
// Broadphase has already done the faster check for collision mask / layers
|
||||
// so no point duplicating
|
||||
|
||||
// Does a contact already exist?
|
||||
if (fixtureA.Contacts.ContainsKey(fixtureB))
|
||||
return;
|
||||
|
||||
DebugTools.Assert(!fixtureA.Contacts.ContainsKey(fixtureB));
|
||||
DebugTools.Assert(!fixtureB.Contacts.ContainsKey(fixtureA));
|
||||
var xformA = entA.Comp2;
|
||||
var xformB = entB.Comp2;
|
||||
|
||||
// Does a joint override collision? Is at least one body dynamic?
|
||||
if (!ShouldCollide(entA.Owner, entB.Owner, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB))
|
||||
if (!ShouldCollideSlow(entA.Owner, entB.Owner, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB))
|
||||
return;
|
||||
|
||||
// Call the factory.
|
||||
@@ -310,14 +307,14 @@ public abstract partial class SharedPhysicsSystem
|
||||
/// <summary>
|
||||
/// Go through the cached broadphase movement and update contacts.
|
||||
/// </summary>
|
||||
internal void AddPair(string fixtureAId, string fixtureBId, in FixtureProxy proxyA, in FixtureProxy proxyB)
|
||||
internal void AddPair(string fixtureAId, string fixtureBId, in FixtureProxy proxyA, in FixtureProxy proxyB, ContactFlags flags = ContactFlags.None)
|
||||
{
|
||||
AddPair((proxyA.Entity, proxyA.Body, proxyA.Xform),
|
||||
(proxyB.Entity, proxyB.Body, proxyB.Xform),
|
||||
fixtureAId, fixtureBId,
|
||||
proxyA.Fixture, proxyA.ChildIndex,
|
||||
proxyB.Fixture, proxyB.ChildIndex,
|
||||
proxyA.Body, proxyB.Body);
|
||||
proxyA.Body, proxyB.Body, flags: flags);
|
||||
}
|
||||
|
||||
internal static bool ShouldCollide(Fixture fixtureA, Fixture fixtureB)
|
||||
@@ -356,8 +353,8 @@ public abstract partial class SharedPhysicsSystem
|
||||
{
|
||||
var ev1 = new EndCollideEvent(aUid, bUid, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB);
|
||||
var ev2 = new EndCollideEvent(bUid, aUid, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA);
|
||||
RaiseLocalEvent(aUid, ref ev1);
|
||||
RaiseLocalEvent(bUid, ref ev2);
|
||||
_endCollideEvents[_endEventIndex].Add(ev1);
|
||||
_endCollideEvents[_endEventIndex].Add(ev2);
|
||||
}
|
||||
|
||||
if (contact.Manifold.PointCount > 0 && contact.FixtureA?.Hard == true && contact.FixtureB?.Hard == true)
|
||||
@@ -447,7 +444,8 @@ public abstract partial class SharedPhysicsSystem
|
||||
{
|
||||
// Check default filtering
|
||||
if (!ShouldCollide(fixtureA, fixtureB) ||
|
||||
!ShouldCollide(uidA, uidB, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB))
|
||||
!ShouldCollideSlow(uidA, uidB, bodyA, bodyB, fixtureA, fixtureB, xformA, xformB) ||
|
||||
!ShouldCollideJoints(uidA, uidB))
|
||||
{
|
||||
DestroyContact(contact);
|
||||
continue;
|
||||
@@ -577,57 +575,7 @@ public abstract partial class SharedPhysicsSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (status[i])
|
||||
{
|
||||
case ContactStatus.StartTouching:
|
||||
{
|
||||
if (!contact.IsTouching) continue;
|
||||
|
||||
var fixtureA = contact.FixtureA!;
|
||||
var fixtureB = contact.FixtureB!;
|
||||
var bodyA = contact.BodyA!;
|
||||
var bodyB = contact.BodyB!;
|
||||
var uidA = contact.EntityA;
|
||||
var uidB = contact.EntityB;
|
||||
var worldPoint = worldPoints[i];
|
||||
var points = new FixedArray2<Vector2>(worldPoint._00, worldPoint._01);
|
||||
var worldNormal = worldPoint._02;
|
||||
|
||||
var ev1 = new StartCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB, points, contact.Manifold.PointCount, worldNormal);
|
||||
var ev2 = new StartCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA, points, contact.Manifold.PointCount, worldNormal);
|
||||
|
||||
RaiseLocalEvent(uidA, ref ev1, true);
|
||||
RaiseLocalEvent(uidB, ref ev2, true);
|
||||
break;
|
||||
}
|
||||
case ContactStatus.Touching:
|
||||
break;
|
||||
case ContactStatus.EndTouching:
|
||||
{
|
||||
var fixtureA = contact.FixtureA;
|
||||
var fixtureB = contact.FixtureB;
|
||||
|
||||
// If something under StartCollideEvent potentially nukes other contacts (e.g. if the entity is deleted)
|
||||
// then we'll just skip the EndCollide.
|
||||
if (fixtureA == null || fixtureB == null) continue;
|
||||
|
||||
var bodyA = contact.BodyA!;
|
||||
var bodyB = contact.BodyB!;
|
||||
var uidA = contact.EntityA;
|
||||
var uidB = contact.EntityB;
|
||||
|
||||
var ev1 = new EndCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB);
|
||||
var ev2 = new EndCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA);
|
||||
|
||||
RaiseLocalEvent(uidA, ref ev1);
|
||||
RaiseLocalEvent(uidB, ref ev2);
|
||||
break;
|
||||
}
|
||||
case ContactStatus.NoContact:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
RunContactEvents(status[i], contact, worldPoints[i]);
|
||||
}
|
||||
|
||||
ArrayPool<Contact>.Shared.Return(contacts);
|
||||
@@ -635,6 +583,61 @@ public abstract partial class SharedPhysicsSystem
|
||||
ArrayPool<FixedArray4<Vector2>>.Shared.Return(worldPoints);
|
||||
}
|
||||
|
||||
internal void RunContactEvents(ContactStatus status, Contact contact, FixedArray4<Vector2> worldPoint)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case ContactStatus.StartTouching:
|
||||
{
|
||||
if (!contact.IsTouching) return;
|
||||
|
||||
var fixtureA = contact.FixtureA!;
|
||||
var fixtureB = contact.FixtureB!;
|
||||
var bodyA = contact.BodyA!;
|
||||
var bodyB = contact.BodyB!;
|
||||
var uidA = contact.EntityA;
|
||||
var uidB = contact.EntityB;
|
||||
var points = new FixedArray2<Vector2>(worldPoint._00, worldPoint._01);
|
||||
var worldNormal = worldPoint._02;
|
||||
|
||||
var ev1 = new StartCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB, points, contact.Manifold.PointCount, worldNormal);
|
||||
var ev2 = new StartCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA, points, contact.Manifold.PointCount, worldNormal);
|
||||
|
||||
_startCollideEvents.Add(ev1);
|
||||
_startCollideEvents.Add(ev2);
|
||||
|
||||
break;
|
||||
}
|
||||
case ContactStatus.Touching:
|
||||
break;
|
||||
case ContactStatus.EndTouching:
|
||||
{
|
||||
var fixtureA = contact.FixtureA;
|
||||
var fixtureB = contact.FixtureB;
|
||||
|
||||
// If something under StartCollideEvent potentially nukes other contacts (e.g. if the entity is deleted)
|
||||
// then we'll just skip the EndCollide.
|
||||
if (fixtureA == null || fixtureB == null) return;
|
||||
|
||||
var bodyA = contact.BodyA!;
|
||||
var bodyB = contact.BodyB!;
|
||||
var uidA = contact.EntityA;
|
||||
var uidB = contact.EntityB;
|
||||
|
||||
var ev1 = new EndCollideEvent(uidA, uidB, contact.FixtureAId, contact.FixtureBId, fixtureA, fixtureB, bodyA, bodyB);
|
||||
var ev2 = new EndCollideEvent(uidB, uidA, contact.FixtureBId, contact.FixtureAId, fixtureB, fixtureA, bodyB, bodyA);
|
||||
|
||||
_endCollideEvents[_endEventIndex].Add(ev1);
|
||||
_endCollideEvents[_endEventIndex].Add(ev2);
|
||||
break;
|
||||
}
|
||||
case ContactStatus.NoContact:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildManifolds(Contact[] contacts, int count, ContactStatus[] status, FixedArray4<Vector2>[] worldPoints)
|
||||
{
|
||||
if (count == 0)
|
||||
@@ -720,10 +723,31 @@ public abstract partial class SharedPhysicsSystem
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is there a joint blocking collision between these bodies.
|
||||
/// </summary>
|
||||
internal bool ShouldCollideJoints(Entity<JointComponent?> entA, Entity<JointComponent?> entB)
|
||||
{
|
||||
// Does a joint prevent collision?
|
||||
// if one of them doesn't have jointcomp then they can't share a common joint.
|
||||
// otherwise, only need to iterate over the joints of one component as they both store the same joint.
|
||||
if (JointQuery.Resolve(entA.Owner, ref entA.Comp, false) && JointQuery.HasComp(entB))
|
||||
{
|
||||
foreach (var joint in entA.Comp.Joints.Values)
|
||||
{
|
||||
// Check if either: the joint even allows collisions OR the other body on the joint is actually the other body we're checking.
|
||||
if (!joint.CollideConnected && (entB.Owner == joint.BodyAUid || entB.Owner == joint.BodyBUid))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to prevent bodies from colliding; may lie depending on joints.
|
||||
/// </summary>
|
||||
protected bool ShouldCollide(
|
||||
internal bool ShouldCollideSlow(
|
||||
EntityUid uid,
|
||||
EntityUid other,
|
||||
PhysicsComponent body,
|
||||
@@ -757,18 +781,7 @@ public abstract partial class SharedPhysicsSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
// Does a joint prevent collision?
|
||||
// if one of them doesn't have jointcomp then they can't share a common joint.
|
||||
// otherwise, only need to iterate over the joints of one component as they both store the same joint.
|
||||
if (TryComp(uid, out JointComponent? jointComponentA) && HasComp<JointComponent>(other))
|
||||
{
|
||||
foreach (var joint in jointComponentA.Joints.Values)
|
||||
{
|
||||
// Check if either: the joint even allows collisions OR the other body on the joint is actually the other body we're checking.
|
||||
if (!joint.CollideConnected && (other == joint.BodyAUid || other == joint.BodyBUid))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Joints already handled before the contact pair is made.
|
||||
|
||||
var preventCollideMessage = new PreventCollideEvent(uid, other, body, otherBody, fixture, otherFixture);
|
||||
RaiseLocalEvent(uid, ref preventCollideMessage);
|
||||
|
||||
30
Robust.Shared/Physics/Systems/SharedPhysicsSystem.Events.cs
Normal file
30
Robust.Shared/Physics/Systems/SharedPhysicsSystem.Events.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace Robust.Shared.Physics.Systems;
|
||||
|
||||
public abstract partial class SharedPhysicsSystem
|
||||
{
|
||||
protected void DispatchEvents()
|
||||
{
|
||||
// This will raise events even if the contact is gone which is fine I think
|
||||
// because otherwise we may get issues with events not getting raised in some cases.
|
||||
|
||||
// Swap the end index over so new events happen next tick.
|
||||
_endEventIndex = 1 - _endEventIndex;
|
||||
|
||||
// Raises all the buffered events once physics step is done.
|
||||
foreach (var ev in _startCollideEvents)
|
||||
{
|
||||
var elem = ev;
|
||||
RaiseLocalEvent(ev.OurEntity, ref elem);
|
||||
}
|
||||
|
||||
_startCollideEvents.Clear();
|
||||
|
||||
foreach (var ev in _endCollideEvents[1 - _endEventIndex])
|
||||
{
|
||||
var elem = ev;
|
||||
RaiseLocalEvent(ev.OurEntity, ref elem);
|
||||
}
|
||||
|
||||
_endCollideEvents[1 - _endEventIndex].Clear();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Prometheus;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Containers;
|
||||
@@ -50,6 +51,25 @@ namespace Robust.Shared.Physics.Systems
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly CollisionWakeSystem _wakeSystem = default!;
|
||||
|
||||
/*
|
||||
* Events
|
||||
*/
|
||||
|
||||
// Buffer events to avoid enumeration issues.
|
||||
|
||||
private readonly List<StartCollideEvent> _startCollideEvents = new();
|
||||
|
||||
// Double-buffer end-collide events like box2d-v3 because we can raise these while destroying contacts
|
||||
// whereas with start events they only ever get made during the collision step.
|
||||
private readonly List<EndCollideEvent>[]
|
||||
_endCollideEvents =
|
||||
[
|
||||
new List<EndCollideEvent>(),
|
||||
new List<EndCollideEvent>()
|
||||
];
|
||||
|
||||
private int _endEventIndex = 0;
|
||||
|
||||
private int _substeps;
|
||||
|
||||
/// <summary>
|
||||
@@ -302,6 +322,8 @@ namespace Robust.Shared.Physics.Systems
|
||||
|
||||
Step(frameTime, prediction);
|
||||
|
||||
DispatchEvents();
|
||||
|
||||
var updateAfterSolve = new PhysicsUpdateAfterSolveEvent(prediction, frameTime);
|
||||
RaiseLocalEvent(ref updateAfterSolve);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user