Files
HellionChat/HellionChat/FontManager.cs
T
JonKazama-Hellion fee2459e73 refactor(services): route logging through IPluginLogProxy
F12.2 step 5b — service cluster (~42 sites in 16 files):
MessageManager, GameFunctions/{Chat, GameFunctions, KeybindManager},
EmoteCache, PayloadHandler, AutoTellTabsService, FontManager, Commands,
Util/{WrapperUtil, AutoTranslate, MemoryUtil}, Message, Themes/ThemeRegistry,
Ipc/ExtraChat, Configuration.

The proxy interface gained Dalamud's params-overload signature
(messageTemplate + params object[]) to cover Configuration.cs:86 which
relies on Serilog-style placeholders.

Verified: zero remaining Plugin.Log.X(...) call-sites in HellionChat/,
build green, build-suite 690/690.
2026-05-13 08:38:40 +02:00

259 lines
8.9 KiB
C#

using Dalamud;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Utility;
namespace HellionChat;
public class FontManager
{
internal IFontHandle Axis = null!;
internal IFontHandle AxisItalic = null!;
internal IFontHandle RegularFont = null!;
internal IFontHandle? ItalicFont;
internal IFontHandle FontAwesome = null!;
private ushort[] Ranges = [];
private ushort[] JpRange = [];
public static readonly HashSet<float> AxisFontSizeList =
[
9.6f,
10f,
12f,
14f,
16f,
18f,
18.4f,
20f,
23f,
34f,
36f,
40f,
45f,
46f,
68f,
90f,
];
// Hellion font bytes (Exo 2, OFL-1.1); lazily loaded from manifest resources
private static byte[]? HellionFontBytes;
// Returns null when the embedded font resource is missing. Should never
// happen on a signed release build, but a broken csproj or hand-rolled
// dev build can land here. Caller falls back to the system font path so
// the plugin still loads instead of crashing the whole UiBuilder.
private static byte[]? TryGetHellionFontBytes()
{
if (HellionFontBytes is not null)
return HellionFontBytes;
using var stream = typeof(FontManager).Assembly.GetManifestResourceStream(
"HellionFont.ttf"
);
if (stream is null)
{
Plugin.LogProxy.Warning(
"Hellion font resource missing — falling back to system default font."
);
return null;
}
using var ms = new MemoryStream();
stream.CopyTo(ms);
HellionFontBytes = ms.ToArray();
return HellionFontBytes;
}
private unsafe void SetUpRanges()
{
ushort[] BuildRange(IReadOnlyList<ushort>? chars, params nint[] ranges)
{
var builder = new ImFontGlyphRangesBuilderPtr(ImGuiNative.ImFontGlyphRangesBuilder());
foreach (var range in ranges)
builder.AddRanges((ushort*)range);
if (chars != null)
{
for (var i = 0; i < chars.Count; i += 2)
{
if (chars[i] == 0)
break;
for (var j = (uint)chars[i]; j <= chars[i + 1]; j++)
builder.AddChar((ushort)j);
}
}
// Ingame supported ranges
var reader = new FdtReader(Plugin.DataManager.GetFile("common/font/axis_12.fdt")!.Data);
foreach (var c in reader.Glyphs)
builder.AddChar(c.Char);
// various symbols
// French
// Romanian
// builder.AddText("←→↑↓《》■※☀★★☆♥♡ヅツッシ☀☁☂℃℉°♀♂♠♣♦♣♧®©™€$£♯♭♪✓√◎◆◇♦■□〇●△▽▼▲‹›≤≥<«“”─\~");
builder.AddText("Œœ");
builder.AddText("ĂăÂâÎîȘșȚț");
// "Enclosed Alphanumerics" (partial) https://www.compart.com/en/unicode/block/U+2460
for (var i = 0x2460; i <= 0x24B5; i++)
builder.AddChar((char)i);
builder.AddChar('⓪');
return builder.BuildRangesToArray();
}
var ranges = new List<nint> { (nint)ImGui.GetIO().Fonts.GetGlyphRangesDefault() };
foreach (var extraRange in Enum.GetValues<ExtraGlyphRanges>())
if (Plugin.Config.ExtraGlyphRanges.HasFlag(extraRange))
ranges.Add(extraRange.Range());
Ranges = BuildRange(null, ranges.ToArray());
JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges);
}
// CPU-bound build offloaded to Task.Run; runs parallel with theme init
public async Task BuildFontsAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Run(BuildFonts, cancellationToken).ConfigureAwait(false);
}
public void BuildFonts()
{
SetUpRanges();
Axis = Plugin.Interface.UiBuilder.FontAtlas.NewGameFontHandle(
new GameFontStyle(GameFontFamily.Axis, SizeInPx(Plugin.Config.FontSizeV2))
);
AxisItalic = Plugin.Interface.UiBuilder.FontAtlas.NewGameFontHandle(
new GameFontStyle(GameFontFamily.Axis, SizeInPx(Plugin.Config.FontSizeV2))
{
SkewStrength = SizeInPx(Plugin.Config.FontSizeV2) / 6,
}
);
FontAwesome = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
{
e.OnPreBuild(tk =>
tk.AddFontAwesomeIconFont(new SafeFontConfig { SizePx = GetFontSize() })
);
e.OnPostBuild(tk => tk.FitRatio(tk.Font));
});
RegularFont = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
e.OnPreBuild(tk =>
{
// v1.2.0: UseHellionFont controls font size selection
var basePt = Plugin.Config.UseHellionFont
? Plugin.Config.FontSizeV2
: Plugin.Config.GlobalFontV2.SizePt;
var config = new SafeFontConfig { SizePt = basePt, GlyphRanges = Ranges };
// F10.2: if the embedded font is missing, drop to the system font
// path rather than letting the UiBuilder throw.
var hellionBytes = Plugin.Config.UseHellionFont ? TryGetHellionFontBytes() : null;
config.MergeFont = hellionBytes is not null
? tk.AddFontFromMemory(hellionBytes, config, "Hellion-Exo2")
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
config.GlyphRanges = JpRange;
AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese");
config.SizePt = Plugin.Config.SymbolsFontSizeV2;
tk.AddGameSymbol(config);
tk.Font = config.MergeFont;
})
);
if (Plugin.Config.ItalicEnabled)
{
ItalicFont = Plugin.Interface.UiBuilder.FontAtlas.NewDelegateFontHandle(e =>
e.OnPreBuild(tk =>
{
var config = new SafeFontConfig
{
SizePt = Plugin.Config.ItalicFontV2.SizePt,
GlyphRanges = Ranges,
};
config.MergeFont = AddFontWithFallback(
tk,
Plugin.Config.ItalicFontV2.FontId,
config,
"italic"
);
config.SizePt = Plugin.Config.JapaneseFontV2.SizePt;
config.GlyphRanges = JpRange;
AddFontWithFallback(
tk,
Plugin.Config.JapaneseFontV2.FontId,
config,
"japanese"
);
config.SizePt = Plugin.Config.SymbolsFontSizeV2;
tk.AddGameSymbol(config);
tk.Font = config.MergeFont;
})
);
}
else
{
ItalicFont = null;
}
}
// Add font with fallback to NotoSansCjkRegular if unavailable
private static ImFontPtr AddFontWithFallback(
IFontAtlasBuildToolkitPreBuild tk,
IFontId fontId,
SafeFontConfig config,
string slot
)
{
try
{
return fontId.AddToBuildToolkit(tk, config);
}
catch (Exception e)
when (e
is FileNotFoundException
or DirectoryNotFoundException
or IOException
or InvalidOperationException
or ArgumentException
)
{
// Atlas-toolkit throws span IO and validation failures; routing the
// wider set through the fallback keeps a corrupt font config from
// taking down the whole atlas build.
Plugin.LogProxy.Warning(
e,
$"Configured {slot} font failed to load ({e.GetType().Name}), "
+ "falling back to NotoSansCjkRegular"
);
var fallback = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular);
return fallback.AddToBuildToolkit(tk, config);
}
}
public static float SizeInPt(float px) => (float)(px * 3.0 / 4.0);
public static float SizeInPx(float pt) => (float)(pt * 4.0 / 3.0);
public static float GetFontSize() =>
Plugin.Config.FontsEnabled
? Plugin.Config.GlobalFontV2.SizePx
: SizeInPx(Plugin.Config.FontSizeV2);
}