diff --git a/Directory.Packages.props b/Directory.Packages.props
index addbf98dd53..019d467378e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -62,6 +62,7 @@
+
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 05aee74aa29..2adb16a2624 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -46,6 +46,9 @@ END TEMPLATE-->
* 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.
### Bugfixes
diff --git a/Robust.Client/ClientIoC.cs b/Robust.Client/ClientIoC.cs
index bb83231862a..15d25740fdc 100644
--- a/Robust.Client/ClientIoC.cs
+++ b/Robust.Client/ClientIoC.cs
@@ -8,6 +8,7 @@
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;
@@ -121,6 +122,8 @@ public static void RegisterIoC(GameController.DisplayMode mode, IDependencyColle
deps.Register();
deps.Register();
deps.Register();
+ deps.Register();
+ deps.Register();
break;
case GameController.DisplayMode.Clyde:
deps.Register();
@@ -131,6 +134,8 @@ public static void RegisterIoC(GameController.DisplayMode mode, IDependencyColle
deps.Register();
deps.Register();
deps.Register();
+ deps.Register();
+ deps.Register();
break;
default:
throw new ArgumentOutOfRangeException();
diff --git a/Robust.Client/GameController/GameController.cs b/Robust.Client/GameController/GameController.cs
index 301e0598745..5591f588124 100644
--- a/Robust.Client/GameController/GameController.cs
+++ b/Robust.Client/GameController/GameController.cs
@@ -96,6 +96,7 @@ internal sealed partial class GameController : IGameControllerInternal
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IReloadManager _reload = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
+ [Dependency] private readonly ISystemFontManagerInternal _systemFontManager = default!;
private IWebViewManagerHook? _webViewHook;
@@ -143,6 +144,7 @@ internal bool StartupContinue(DisplayMode displayMode)
_taskManager.Initialize();
_parallelMgr.Initialize();
_fontManager.SetFontDpi((uint)_configurationManager.GetCVar(CVars.DisplayFontDpi));
+ _systemFontManager.Initialize();
// Load optional Robust modules.
LoadOptionalRobustModules(displayMode, _resourceManifest!);
diff --git a/Robust.Client/Graphics/Font.cs b/Robust.Client/Graphics/Font.cs
index da627769c2e..e1f16b92f27 100644
--- a/Robust.Client/Graphics/Font.cs
+++ b/Robust.Client/Graphics/Font.cs
@@ -104,6 +104,12 @@ public VectorFont(FontResource res, int size)
Handle = IoCManager.Resolve().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 @@ public override float DrawChar(DrawingHandleBase handle, Rune rune, Vector2 base
return null;
}
}
+
+ ///
+ /// Possible values for font weights. Larger values have thicker font strokes.
+ ///
+ ///
+ ///
+ /// These values are based on the usWeightClass property of the OpenType specification:
+ /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass
+ ///
+ ///
+ ///
+ 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,
+ }
+
+ ///
+ /// Possible slant values for fonts.
+ ///
+ ///
+ 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
+ }
+
+ ///
+ /// Possible values for font widths. Larger values are proportionally wider.
+ ///
+ ///
+ ///
+ /// These values are based on the usWidthClass property of the OpenType specification:
+ /// https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass
+ ///
+ ///
+ ///
+ public enum FontWidth : ushort
+ {
+ UltraCondensed = 1,
+ ExtraCondensed = 2,
+ Condensed = 3,
+ SemiCondensed = 4,
+ Normal = 5,
+ Medium = Normal,
+ SemiExpanded = 6,
+ Expanded = 7,
+ ExtraExpanded = 8,
+ UltraExpanded = 9,
+ }
}
diff --git a/Robust.Client/Graphics/FontManagement/SystemFontDebug.cs b/Robust.Client/Graphics/FontManagement/SystemFontDebug.cs
new file mode 100644
index 00000000000..8bead2c1344
--- /dev/null
+++ b/Robust.Client/Graphics/FontManagement/SystemFontDebug.cs
@@ -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();
+ }
+}
diff --git a/Robust.Client/Graphics/FontManagement/SystemFontDebugWindow.xaml b/Robust.Client/Graphics/FontManagement/SystemFontDebugWindow.xaml
new file mode 100644
index 00000000000..bee4a157e3d
--- /dev/null
+++ b/Robust.Client/Graphics/FontManagement/SystemFontDebugWindow.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Robust.Client/Graphics/FontManagement/SystemFontDebugWindow.xaml.cs b/Robust.Client/Graphics/FontManagement/SystemFontDebugWindow.xaml.cs
new file mode 100644
index 00000000000..8061d766fcc
--- /dev/null
+++ b/Robust.Client/Graphics/FontManagement/SystemFontDebugWindow.xaml.cs
@@ -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().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);
+ }
+ }
+}
diff --git a/Robust.Client/Graphics/FontManagement/SystemFontManagerBase.cs b/Robust.Client/Graphics/FontManagement/SystemFontManagerBase.cs
new file mode 100644
index 00000000000..f27d5c37a05
--- /dev/null
+++ b/Robust.Client/Graphics/FontManagement/SystemFontManagerBase.cs
@@ -0,0 +1,165 @@
+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
+{
+ ///
+ /// The "standard" locale used when looking up the PostScript name of a font face.
+ ///
+ ///
+ ///
+ /// 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
+ ///
+ ///
+ 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 Fonts = [];
+
+ public IEnumerable 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 = new()
+ {
+ Primary = "en",
+ Values = new Dictionary { { "en", "" } }
+ };
+
+ ///
+ /// The first locale to appear in the list of localized strings.
+ /// Used as fallback if the desired locale is not provided.
+ ///
+ public required string Primary;
+ public required Dictionary Values;
+ }
+
+ 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();
+ }
+ }
+}
diff --git a/Robust.Client/Graphics/FontManagement/SystemFontManagerDirectWrite.cs b/Robust.Client/Graphics/FontManagement/SystemFontManagerDirectWrite.cs
new file mode 100644
index 00000000000..4f6049f4c6e
--- /dev/null
+++ b/Robust.Client/Graphics/FontManagement/SystemFontManagerDirectWrite.cs
@@ -0,0 +1,503 @@
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Reflection.Metadata;
+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;
+
+///
+/// Implementation of that uses DirectWrite on Windows.
+///
+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;
+
+ ///
+ /// Implementation of that uses DirectWrite on Windows.
+ ///
+ 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(),
+ (IUnknown**)pFactory);
+
+ ThrowIfFailed(result);
+ }
+ }
+
+ private IDWriteFontSet* GetSystemFontSet(IDWriteFactory3* factory)
+ {
+ IDWriteFactory6* factory6;
+ IDWriteFontSet* fontSet;
+ var result = factory->QueryInterface(__uuidof(), (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(), (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(), (void**)&list));
+
+ try
+ {
+ return GetStrings(list);
+ }
+ finally
+ {
+ list->Release();
+ }
+ }
+
+ private static DWriteLocalizedString[] GetStrings(IDWriteStringList* stringList)
+ {
+ var array = new DWriteLocalizedString[stringList->GetCount()];
+
+ var stringPool = ArrayPool.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.Shared.Return(stringPool);
+
+ return array;
+ }
+
+ private static void ExpandIfNecessary(ref char[] array, uint requiredLength)
+ {
+ if (requiredLength < array.Length)
+ return;
+
+ ArrayPool.Shared.Return(array);
+ array = ArrayPool.Shared.Rent(checked((int)requiredLength));
+ }
+
+ private static LocalizedStringSet StringsToSet(DWriteLocalizedString[] strings)
+ {
+ var dict = new Dictionary();
+
+ 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;
+ }
+
+ ///
+ /// A simple implementation of a .NET Stream over a IDWriteFontFileStream.
+ ///
+ 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 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(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);
+}
diff --git a/Robust.Client/Graphics/FontManagement/SystemFontManagerFallback.cs b/Robust.Client/Graphics/FontManagement/SystemFontManagerFallback.cs
new file mode 100644
index 00000000000..31638699eca
--- /dev/null
+++ b/Robust.Client/Graphics/FontManagement/SystemFontManagerFallback.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+
+namespace Robust.Client.Graphics.FontManagement;
+
+///
+/// A fallback implementation of that just loads no fonts.
+///
+internal sealed class SystemFontManagerFallback : ISystemFontManagerInternal
+{
+ public void Initialize()
+ {
+
+ }
+
+ public void Shutdown()
+ {
+
+ }
+
+ public bool IsSupported => false;
+ public IEnumerable SystemFontFaces => [];
+}
diff --git a/Robust.Client/Graphics/FontManagement/SystemFontManagerFontconfig.cs b/Robust.Client/Graphics/FontManagement/SystemFontManagerFontconfig.cs
new file mode 100644
index 00000000000..6f4b1701248
--- /dev/null
+++ b/Robust.Client/Graphics/FontManagement/SystemFontManagerFontconfig.cs
@@ -0,0 +1,233 @@
+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 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 @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 @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(FcPattern* p, sbyte* o, int i, out T value);
+ private static unsafe T[]? PatternGetValues(FcPattern* pattern, ReadOnlySpan @object, GetValue getValue)
+ {
+ var list = new List();
+
+ 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 @object, ReadOnlySpan 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();
+
+ 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;
+ }
+}
diff --git a/Robust.Client/Graphics/FontManager.cs b/Robust.Client/Graphics/FontManager.cs
index 19c0d410a9f..5964753bb28 100644
--- a/Robust.Client/Graphics/FontManager.cs
+++ b/Robust.Client/Graphics/FontManager.cs
@@ -1,16 +1,15 @@
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.Maths;
using Robust.Shared.Utility;
using SharpFont;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
-using TerraFX.Interop.Windows;
namespace Robust.Client.Graphics
{
@@ -34,13 +33,18 @@ public FontManager(IClyde clyde)
_library = new Library();
}
- 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 unsafe IFontFaceHandle Load(IFontMemoryHandle memory, int index = 0)
+ {
+ var face = new Face(_library, (nint) memory.GetData(), checked((int)memory.GetDataSize()), index);
+ var handle = new FontFaceHandle(face, memory);
return handle;
}
@@ -235,10 +239,13 @@ private static Image MonoBitMapToImage(FTBitmap bitmap)
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 +384,32 @@ public sealed class GlyphInfo
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();
+ }
+ }
}
}
diff --git a/Robust.Client/Graphics/IFontManager.cs b/Robust.Client/Graphics/IFontManager.cs
index 34cacb93dc0..c8b994c4e58 100644
--- a/Robust.Client/Graphics/IFontManager.cs
+++ b/Robust.Client/Graphics/IFontManager.cs
@@ -1,3 +1,4 @@
+using System;
using System.IO;
using System.Text;
using Robust.Shared.Graphics;
@@ -10,7 +11,8 @@ public interface IFontManager
}
internal interface IFontManagerInternal : IFontManager
{
- IFontFaceHandle Load(Stream stream);
+ IFontFaceHandle Load(Stream stream, int index = 0);
+ IFontFaceHandle Load(IFontMemoryHandle memory, int index = 0);
IFontInstanceHandle MakeInstance(IFontFaceHandle handle, int size);
void SetFontDpi(uint fontDpi);
}
@@ -22,8 +24,6 @@ internal interface IFontFaceHandle
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 +35,12 @@ internal interface IFontInstanceHandle
int GetLineHeight(float scale);
}
+ internal unsafe interface IFontMemoryHandle : IDisposable
+ {
+ byte* GetData();
+ nint GetDataSize();
+ }
+
///
/// Metrics for a single glyph in a font.
/// Refer to https://www.freetype.org/freetype2/docs/glyphs/glyphs-3.html for more information.
diff --git a/Robust.Client/Graphics/ISystemFontManager.cs b/Robust.Client/Graphics/ISystemFontManager.cs
new file mode 100644
index 00000000000..b96451bf924
--- /dev/null
+++ b/Robust.Client/Graphics/ISystemFontManager.cs
@@ -0,0 +1,127 @@
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace Robust.Client.Graphics;
+
+///
+/// Provides access to fonts installed on the user's operating system.
+///
+///
+///
+/// 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.
+///
+///
+///
+public interface ISystemFontManager
+{
+ ///
+ /// Whether access to system fonts is currently supported on this platform.
+ ///
+ bool IsSupported { get; }
+
+ ///
+ /// The list of font face available from the operating system.
+ ///
+ IEnumerable SystemFontFaces { get; }
+}
+
+///
+/// A single font face, provided by the user's operating system.
+///
+///
+public interface ISystemFontFace
+{
+ ///
+ /// The PostScript name of the font face.
+ /// This is generally the closest to an unambiguous unique identifier as you're going to get.
+ ///
+ ///
+ ///
+ /// For example, "Arial-ItalicMT"
+ ///
+ ///
+ string PostscriptName { get; }
+
+ ///
+ /// The full name of the font face, localized to the current locale.
+ ///
+ ///
+ ///
+ /// For example, "Arial Cursiva"
+ ///
+ ///
+ ///
+ string FullName { get; }
+
+ ///
+ /// The family name of the font face, localized to the current locale.
+ ///
+ ///
+ ///
+ /// For example, "Arial"
+ ///
+ ///
+ ///
+ string FamilyName { get; }
+
+ ///
+ /// The face name (or "style name") of the font face, localized to the current locale.
+ ///
+ ///
+ ///
+ /// For example, "Cursiva"
+ ///
+ ///
+ ///
+ string FaceName { get; }
+
+ ///
+ /// Get the , localized to a specific locale.
+ ///
+ /// The locale to fetch the localized string for.
+ string GetLocalizedFullName(CultureInfo culture);
+
+ ///
+ /// Get the , localized to a specific locale.
+ ///
+ /// The locale to fetch the localized string for.
+ string GetLocalizedFamilyName(CultureInfo culture);
+
+ ///
+ /// Get the , localized to a specific locale.
+ ///
+ /// The locale to fetch the localized string for.
+ string GetLocalizedFaceName(CultureInfo culture);
+
+ ///
+ /// The weight of the font face.
+ ///
+ FontWeight Weight { get; }
+
+ ///
+ /// The slant of the font face.
+ ///
+ FontSlant Slant { get; }
+
+ ///
+ /// The width of the font face.
+ ///
+ FontWidth Width { get; }
+
+ ///
+ /// Load the font face so that it can be used in-engine.
+ ///
+ /// The size to load the font at.
+ /// A font object that can be used to render text.
+ Font Load(int size);
+}
+
+///
+/// Engine-internal API for .
+///
+internal interface ISystemFontManagerInternal : ISystemFontManager
+{
+ void Initialize();
+ void Shutdown();
+}
diff --git a/Robust.Client/Graphics/SystemFontManager.cs b/Robust.Client/Graphics/SystemFontManager.cs
new file mode 100644
index 00000000000..939638df2d4
--- /dev/null
+++ b/Robust.Client/Graphics/SystemFontManager.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Collections.Generic;
+using Robust.Client.Graphics.FontManagement;
+using Robust.Shared.Configuration;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Timing;
+
+namespace Robust.Client.Graphics;
+
+///
+/// Implementation of that proxies to platform-specific implementations,
+/// and adds additional logging.
+///
+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 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 (OperatingSystem.IsWindows())
+ return new SystemFontManagerDirectWrite(_logManager, _cfg, _fontManager);
+
+ if (OperatingSystem.IsLinux() || OperatingSystem.IsFreeBSD())
+ return new SystemFontManagerFontconfig(_logManager, _fontManager);
+
+ // TODO: Linux and macOS implementations.
+
+ return new SystemFontManagerFallback();
+ }
+
+ void IPostInjectInit.PostInject()
+ {
+ _sawmill = _logManager.GetSawmill("font.system");
+ // _sawmill.Level = LogLevel.Verbose;
+ }
+}
diff --git a/Robust.Client/Robust.Client.csproj b/Robust.Client/Robust.Client.csproj
index c1df156554f..7f8f3d3649a 100644
--- a/Robust.Client/Robust.Client.csproj
+++ b/Robust.Client/Robust.Client.csproj
@@ -27,6 +27,7 @@
+
diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs
index 1996c21ca09..07007de3318 100644
--- a/Robust.Shared/CVars.cs
+++ b/Robust.Shared/CVars.cs
@@ -1924,5 +1924,15 @@ internal static readonly CVarDef
///
public static readonly CVarDef XamlHotReloadMarkerName =
CVarDef.Create("ui.xaml_hot_reload_marker_name", "SpaceStation14.sln", CVar.CLIENTONLY);
+
+ /*
+ * FONT
+ */
+
+ ///
+ /// If true, allow Windows "downloadable" fonts to be exposed to the system fonts API.
+ ///
+ public static readonly CVarDef FontWindowsDownloadable =
+ CVarDef.Create("font.windows_downloadable", false, CVar.CLIENTONLY | CVar.ARCHIVE);
}
}