b9d3ff8f26
The atlas-toolkit pipeline can throw InvalidOperationException or ArgumentException when a configured font is structurally broken (e.g. unreadable header, unsupported glyph table). Previously only IO-shaped throws routed to the NotoSansCjkRegular fallback, so a corrupt font config would take down the entire atlas build instead of degrading gracefully. The warning log now carries the exception type name so the diagnostic path can tell which class of throw triggered the fallback.
259 lines
8.9 KiB
C#
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.Log.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.Log.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);
|
|
}
|