Compare commits

...

26 Commits

Author SHA1 Message Date
metalgearsloth
c95b4320cf Version: 267.4.0 2025-11-09 18:44:49 +11:00
metalgearsloth
4755cb5747 Better broadphase performance (#6272)
* Better broadphase parallelism

Moves more stuff into the parallel loop and avoids allocating the list per fixtureproxy.

* Fixes

* Better docs

* doc
2025-11-08 02:11:31 +11:00
PJB3005
7bc0ffb711 Make XAML hot reload JIT on UI load
This means we don't have to JIT a bunch of UIs that you might not open, reducing memory usage and startup overhead.

One (1) UI is always JITed in another thread before prototype UIs are loaded, so as to warm up the JIT machinery. Said type is DropDownDebugConsole which always gets used anyways so there's no harm in it.

In total, these changes save more than a second of startup time for me.
2025-10-30 01:41:17 +01:00
beck-thompson
e49515956a Add a basic loading screen! (#6003)
* Added basic loading screen

* Make it look better!

* I forgor xD

* Fix test fails

* Add comment

* Removed unused import

* Only write to file if the number of sections changed

* Servers can now have their own settings

* Minor optionzation and rare colors

* Remove some of the cvars

* debug only loading messages

* Added a few more steps

* Only one section at a time

* nullable section name

* Lock out functions if finished

* Get rid of saving the ccvar

* Cleanup

* Forgot!

* A few tweaks

* Disable vsync

* remove colors

* remove outdated vsync functions

* Silly me xD

* What I get for trying to be clever... ;(

* Better seconds display

* Simplify drawing logic + it looks better

* Type does not need to be partial

* Make interface to expose to content

* Use correct define to gate showing debug info

Should be TOOLS instead of DEBUG

* Use appropriate exception type in BeginLoadingSection

* Fix exception when closing window during loading screen

Would try to stop the main loop before it exists.

* Rename CVars, put debug info behind CVar instead of conditional compilation.

* Add to RELEASE-NOTES.md

* Add UI scaling support

* Make ILoadingScreenManager fully internal

Didn't realize content can't touch it as it'd break the total amount of sections

* Don't re-enable vsync manually, GameController does it at the end of init

* Add command to show top load time usage.

* Improve verbosity of debug time tracking

More steps and some steps named better

---------

Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
2025-10-30 00:38:59 +01:00
Pieter-Jan Briers
f3a3f564e1 System font API (#5393)
* System font API

This is a new API that allows operating system fonts to be loaded by the engine and used by content.

Fonts are provided in a flat list exposing all the relevant metadata. They are loaded from disk with a Load call.

Initial implementation is only for Windows DirectWrite.

* Load system fonts as memory mapped files if possible.

This allows sharing the font file memory with other processes which is always good.

* Use ArrayPool to reduce char array allocations

* Disable verbose logging

* Implement system font support on Linux via Fontconfig

* Implement macOS support

* Add "FREEDESKTOP" define constant

This is basically LINUX || FREEBSD. Though FreeBSD currently gets detected as LINUX too. Oh well.

* Compile out Fontconfig and CoreText system font backends when not on those platforms

* Don't add Fontconfig package dep on Mac/Windows

* Allow disabling system font support via CVar

Cuz why not.
2025-10-28 22:07:55 +01:00
Leon Friedrich
8b7fbfa646 Add two new custom yaml serializers (#6253)
* Add two new custom yaml serializers

* make ComponentNameSerializer ignore ignored components

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-10-27 23:05:46 +11:00
Leon Friedrich
bb4c4ed302 Fix PredictedQueueDeleteEntity mispredicts (#6260)
* Fix PredictedQueueDeleteEntity

* typo

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
2025-10-27 23:04:12 +11:00
PJB3005
c9009342b6 Move CVar registration to before config load on server
Fixes error on startup with the rollback system.
2025-10-26 23:14:11 +01:00
PJB3005
3a337e4842 SECURE CVars are no longer a thing
Good riddance
2025-10-26 23:13:42 +01:00
PJB3005
e788325cdb Expose more StringBuilder overloads to sandbox
Just some stuff that got added in the years. Spans, interpolated string handlers.
2025-10-26 23:09:59 +01:00
PJB3005
9a0e3b6b02 dmetamem now sorts results, no longer outputs to log to avoid interleaving 2025-10-26 23:09:37 +01:00
PJB3005
37eabbabc2 Add MetadataUpdateHandlerAttribute to sandbox 2025-10-26 22:53:06 +01:00
PJB3005
ab775af7cd Added FontTagHijackHolder to replace fonts resolved by FontTag.
Existing font prototype system is extremely half-baked and unusable. This allows bypassing it entirely for upcoming content changes.
2025-10-26 20:40:39 +01:00
PJB3005
8ac5fc58d2 Invalidate OutputPanel on style change
Fixes an incorrect height staying cached on font change.
2025-10-26 20:37:42 +01:00
PJB3005
37c7aa544e Properly use arrange in rich text control layout
This code is still broken, but this at least fixes the fact they don't get arranged with a proper size.

Supersedes #6269
2025-10-26 17:42:37 +01:00
PJB3005
7542b1ca16 Don't do work if assigning stylesheet a control already has 2025-10-26 17:40:19 +01:00
PJB3005
8235bd8478 OptionButton can now be filtered 2025-10-26 17:38:41 +01:00
PJB3005
1657a49c1c Fix modifying Label.FontOverride not causing a layout update. 2025-10-25 17:56:58 +02:00
Myra
669b515ce6 Ensure that sdl3 is the fallback if unknown windowingAPI is specified (#6266)
* Ensure what sdl3 is a fallback if unknown windowingAPI is specified

Webedit ops

* I am blind
2025-10-23 23:43:57 +02:00
metalgearsloth
8478e62a3e Add pure to some transform methods (#6262)
Useful IDE stuff
2025-10-22 18:47:02 +02:00
PJB3005
034728258c Add config rollback system
This is intended for content-side settings menus, so we can show users a "does this look correct" prompt after changing sensitive settings like graphics or UI, without risking an untimely config save *storing* broken CVar config.
2025-10-22 14:09:40 +02:00
PJB3005
b0fec0fd76 CVars defined in [CVarDefs] can now be private or internal. 2025-10-22 14:06:33 +02:00
Leon Friedrich
665294bee8 Rethrow more exceptions when EXCEPTION_TOLERANCE is false (#6238)
* Rethrow more exceptions when EXCEPTION_TOLERANCE is false

* A

* update test

* Revert "update test"

This reverts commit 37f4da67fc.

* actually we probably want to know if Deleting an exception throwing entity throws another exception
2025-10-20 20:51:24 +02:00
PJB3005
4b04081749 Fix Menu and NumpadDecimal key codes on SDL3
Fixes #6255
2025-10-16 14:39:08 +02:00
Amy
d3a9199b8e my hatred for yaml is building (#6226) 2025-10-15 23:28:47 +02:00
PJB3005
6fb9ff7554 Improve viewport leak logging
Shows name

Also fixes erroneous leak logging oops
2025-10-15 01:23:44 +02:00
67 changed files with 3409 additions and 319 deletions

View File

@@ -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" />

View File

@@ -13,7 +13,7 @@
</When>
<Otherwise>
<PropertyGroup>
<DefineConstants>$(DefineConstants);LINUX;UNIX</DefineConstants>
<DefineConstants>$(DefineConstants);LINUX;UNIX;FREEDESKTOP</DefineConstants>
</PropertyGroup>
</Otherwise>
</Choose>

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -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>

View File

@@ -54,6 +54,44 @@ END TEMPLATE-->
*None yet*
## 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

View File

@@ -8,3 +8,5 @@ color-selector-sliders-alpha = A
color-selector-sliders-rgb = RGB
color-selector-sliders-hsv = HSV
option-button-filter = Filter

View File

@@ -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();

View File

@@ -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}""");
}
}
}

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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 />

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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);

View File

@@ -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,
}
}

View 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();
}
}

View File

@@ -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>

View File

@@ -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);
}
}
}

View 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();
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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 => [];
}

View File

@@ -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

View File

@@ -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();
}
}
}
}

View File

@@ -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.

View 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();
}

View 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}");
}
}
}

View 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;
}
}

View 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

View 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

View 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

View File

@@ -141,7 +141,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;

View File

@@ -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" />

View File

@@ -19,6 +19,9 @@ namespace Robust.Client.UserInterface
get => _stylesheet;
set
{
if (ReferenceEquals(_stylesheet, value))
return;
_stylesheet = value;
StylesheetUpdateRecursive();
}

View File

@@ -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
{

View File

@@ -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; }

View File

@@ -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;

View File

@@ -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!;
}

View File

@@ -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;
}
}

View 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();
}
}
}

View File

@@ -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);

View File

@@ -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

View File

@@ -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!;

View File

@@ -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]);

View File

@@ -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>

View File

@@ -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());

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}
}

View 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;
}
}
}

View File

@@ -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)

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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
);

View File

@@ -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)"

View File

@@ -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;
}
@@ -285,12 +288,7 @@ namespace Robust.Shared.GameObjects
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);
@@ -601,6 +609,9 @@ namespace Robust.Shared.GameObjects
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,6 +693,9 @@ 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);

View File

@@ -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);

View File

@@ -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,
}
}
}

View File

@@ -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)
@@ -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;
@@ -720,10 +718,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 +776,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);

View File

@@ -9,7 +9,8 @@ using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
/// <summary>
/// Simple string serializer that just validates that strings correspond to valid component names
/// Simple string serializer that just validates that strings correspond to valid component names.
/// This will not fail when it encounters explicitly ignored components.
/// </summary>
public sealed class ComponentNameSerializer : ITypeSerializer<string, ValueDataNode>
{
@@ -17,7 +18,7 @@ public sealed class ComponentNameSerializer : ITypeSerializer<string, ValueDataN
IDependencyCollection dependencies, ISerializationContext? context = null)
{
var factory = dependencies.Resolve<IComponentFactory>();
if (!factory.TryGetRegistration(node.Value, out _))
if (!factory.TryGetRegistration(node.Value, out _) && factory.GetComponentAvailability(node.Value) != ComponentAvailability.Ignore)
return new ErrorNode(node, $"Unknown component kind: {node.Value}");
return new ValidatedValueNode(node);

View File

@@ -0,0 +1,65 @@
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic;
/// <summary>
/// This is a variant of the normal array serializer that uses a custom type serializer to handle the values.
/// </summary>
public sealed class CustomArraySerializer<T, TCustomSerializer> : ITypeSerializer<T[], SequenceDataNode>
where TCustomSerializer : ITypeSerializer<T, ValueDataNode>
{
T[] ITypeReader<T[], SequenceDataNode>.Read(
ISerializationManager serializationManager,
SequenceDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context,
ISerializationManager.InstantiationDelegate<T[]>? instanceProvider)
{
var list = new T[node.Count];
var i = 0;
foreach (var dataNode in node)
{
list[i++] = serializationManager.Read<T, ValueDataNode, TCustomSerializer>((ValueDataNode)dataNode, hookCtx, context);
}
return list;
}
ValidationNode ITypeValidator<T[], SequenceDataNode>.Validate(
ISerializationManager seri,
SequenceDataNode node,
IDependencyCollection deps,
ISerializationContext? ctx)
{
var list = new List<ValidationNode>(node.Count);
foreach (var elem in node)
{
list.Add(seri.ValidateNode<T, ValueDataNode, TCustomSerializer>((ValueDataNode)elem, ctx));
}
return new ValidatedSequenceNode(list);
}
public DataNode Write(
ISerializationManager seri,
T[] value,
IDependencyCollection deps,
bool alwaysWrite = false,
ISerializationContext? ctx = null)
{
var sequence = new SequenceDataNode();
foreach (var elem in value)
{
sequence.Add(seri.WriteValue<T, TCustomSerializer>(elem, alwaysWrite, ctx));
}
return sequence;
}
}

View File

@@ -0,0 +1,66 @@
using System.Collections.Generic;
using Robust.Shared.IoC;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.Markdown.Value;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Robust.Shared.Serialization.TypeSerializers.Implementations.Generic;
/// <summary>
/// This is a variation of the <see cref="ListSerializers{T}"/> that uses a custom type serializer to handle the values.
/// </summary>
public sealed class CustomListSerializer<T, TCustomSerializer>
: ITypeSerializer<List<T>, SequenceDataNode>
where TCustomSerializer : ITypeSerializer<T, ValueDataNode>
{
List<T> ITypeReader<List<T>, SequenceDataNode>.Read(
ISerializationManager seri,
SequenceDataNode node,
IDependencyCollection deps,
SerializationHookContext hookCtx,
ISerializationContext? ctx,
ISerializationManager.InstantiationDelegate<List<T>>? instanceProvider)
{
var list = instanceProvider != null ? instanceProvider() : new(node.Count);
foreach (var dataNode in node)
{
var value = seri.Read<T, ValueDataNode, TCustomSerializer>((ValueDataNode)dataNode, hookCtx, ctx);
list.Add(value);
}
return list;
}
ValidationNode ITypeValidator<List<T>, SequenceDataNode>.Validate(
ISerializationManager seri,
SequenceDataNode node,
IDependencyCollection deps,
ISerializationContext? ctx)
{
var list = new List<ValidationNode>(node.Count);
foreach (var elem in node)
{
list.Add(seri.ValidateNode<T, ValueDataNode, TCustomSerializer>((ValueDataNode)elem, ctx));
}
return new ValidatedSequenceNode(list);
}
public DataNode Write(
ISerializationManager seri,
List<T> value,
IDependencyCollection deps,
bool alwaysWrite = false,
ISerializationContext? ctx = null)
{
var sequence = new SequenceDataNode();
foreach (var elem in value)
{
sequence.Add(seri.WriteValue<T, TCustomSerializer>(elem, alwaysWrite, ctx));
}
return sequence;
}
}