diff --git a/HellionChat/FontManager.cs b/HellionChat/FontManager.cs index 8b7fb6d..1a3202f 100644 --- a/HellionChat/FontManager.cs +++ b/HellionChat/FontManager.cs @@ -61,6 +61,17 @@ public sealed class FontManager : IDisposable private ushort[] Ranges = []; private ushort[] JpRange = []; + // B1: trimmed remainder the NotoSansCjk fallback is the sole source for + // (Hangul + Simplified-Han); excludes the Default/Latin block already merged + // by the global font, so the fallback no longer re-merges the full Ranges array. + private ushort[] CjkFallbackGlyphRange = []; + + // Report accessor for the ctor self-test: built glyph-range array lengths so + // the step can show the B1 dedup effect (a small trimmed fallback vs the large + // primary range) in its on-disk report instead of a bare Pass. + internal (int Ranges, int JpRange, int CjkFallback) GlyphRangeLengths => + (Ranges.Length, JpRange.Length, CjkFallbackGlyphRange.Length); + public static readonly HashSet AxisFontSizeList = [ 9.6f, @@ -170,6 +181,42 @@ public sealed class FontManager : IDisposable // Instance method so Ranges / JpRange are reachable without parameter // plumbing; PascalCase field names follow the existing class style. + // B1: shared CJK + symbols tail for both the regular and italic delegate + // fonts. Earlier-merged fonts win for shared codepoints (imgui MergeMode), + // so this runs AFTER the primary font is set as config.MergeFont. The CJK + // fallback is the sole Hangul/Simplified-Han source when UseHellionFont=true + // (global=Inter-Light), so it stays in the chain — only its glyph range is + // trimmed (CjkFallbackGlyphRange) to drop the Default-block/endonym overlap. + // basePt sizes the fallback to match the primary font; the Japanese merge + // keeps its own configured size and the full JpRange (which owns Traditional + // Han such as 體 U+9AD4), so japanese↔fallback no longer overlap. + private void AddCjkAndSymbols( + IFontAtlasBuildToolkitPreBuild tk, + SafeFontConfig config, + float basePt + ) + { + config.SizePt = Plugin.Config.JapaneseFontV2.SizePt; + config.GlyphRanges = JpRange; + AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese"); + + // v1.5.3: NotoSansCjk fallback covers Hangul, Simplified-Chinese-specific + // Han (e.g. 简) and other CJK glyphs the primary (Inter Light / global font) + // and the FFXIV Japanese font do not ship. B1: trimmed to CjkFallbackGlyphRange + // so it no longer re-merges the Default block. Merged last so earlier fonts win. + config.SizePt = basePt; + config.GlyphRanges = CjkFallbackGlyphRange; + AddFontWithFallback( + tk, + new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), + config, + "noto-cjk-fallback" + ); + + config.SizePt = ResolveSymbolsFontPt(); + tk.AddGameSymbol(config); + } + private IFontHandle BuildRegularFontHandle(IFontAtlas atlas) => atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => @@ -183,25 +230,7 @@ public sealed class FontManager : IDisposable ? tk.AddFontFromMemory(bundledBytes, config, "Inter-Light") : 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"); - - // v1.5.3: NotoSansCjk fallback covers Hangul, Simplified-Chinese - // -specific Han (e.g. 简) and other CJK glyphs that the primary - // (Inter Light / global font) and the FFXIV Japanese font do not - // ship. Merged last so earlier fonts win for shared codepoints. - config.SizePt = basePt; - config.GlyphRanges = Ranges; - AddFontWithFallback( - tk, - new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), - config, - "noto-cjk-fallback" - ); - - config.SizePt = ResolveSymbolsFontPt(); - tk.AddGameSymbol(config); + AddCjkAndSymbols(tk, config, basePt); tk.Font = config.MergeFont; }) @@ -223,22 +252,7 @@ public sealed class FontManager : IDisposable "italic" ); - config.SizePt = Plugin.Config.JapaneseFontV2.SizePt; - config.GlyphRanges = JpRange; - AddFontWithFallback(tk, Plugin.Config.JapaneseFontV2.FontId, config, "japanese"); - - // v1.5.3: NotoSansCjk fallback (see BuildRegularFontHandle). - config.SizePt = Plugin.Config.ItalicFontV2.SizePt; - config.GlyphRanges = Ranges; - AddFontWithFallback( - tk, - new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), - config, - "noto-cjk-fallback" - ); - - config.SizePt = ResolveSymbolsFontPt(); - tk.AddGameSymbol(config); + AddCjkAndSymbols(tk, config, Plugin.Config.ItalicFontV2.SizePt); tk.Font = config.MergeFont; }) @@ -282,7 +296,11 @@ public sealed class FontManager : IDisposable private unsafe void SetUpRanges() { - ushort[] BuildRange(IReadOnlyList? chars, params nint[] ranges) + ushort[] BuildRange( + IReadOnlyList? chars, + bool includeCommonExtras, + params nint[] ranges + ) { var builder = new ImFontGlyphRangesBuilderPtr(ImGuiNative.ImFontGlyphRangesBuilder()); foreach (var range in ranges) @@ -300,33 +318,43 @@ public sealed class FontManager : IDisposable } } - // 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); + // Common extras (Axis ingame glyphs, endonyms, enclosed alphanumerics) + // belong to the primary/Japanese ranges only. The trimmed CJK fallback + // (B1) skips them so it stays a pure Hangul/Simplified-Han remainder and + // does not re-merge the Default-block work the global font already did. + if (includeCommonExtras) + { + // 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); - // French - // Romanian - builder.AddText("Œœ"); - builder.AddText("ĂăÂâÎîȘșȚț"); + // French + // Romanian + builder.AddText("Œœ"); + builder.AddText("ĂăÂâÎîȘșȚț"); - // v1.5.3: language-dropdown endonyms. The dropdown renders - // with the currently active font range; without these glyphs - // a user on an English UI cannot read non-Latin language names - // before switching. Auto-activation in Settings.Apply then - // pulls in the full ExtraGlyphRange for the chosen locale. - builder.AddText( - "Català Čeština Dansk Deutsch Ελληνικά English Español Suomi" - + " Français Magyar Italiano 日本語 한국어 Norsk bokmål Nederlands" - + " Polski Português Brasil (Portugal) Română Русский Svenska" - + " Türkçe Українська 简体中文 繁體中文" - ); + // v1.5.3: language-dropdown endonyms. The dropdown renders + // with the currently active font range; without these glyphs + // a user on an English UI cannot read non-Latin language names + // before switching. Auto-activation in Settings.Apply then + // pulls in the full ExtraGlyphRange for the chosen locale. + builder.AddText( + "Català Čeština Dansk Deutsch Ελληνικά English Español Suomi" + + " Français Magyar Italiano 日本語 한국어 Norsk bokmål Nederlands" + + " Polski Português Brasil (Portugal) Română Русский Svenska" + + " Türkçe Українська 简体中文 繁體中文" + ); - // "Enclosed Alphanumerics" (partial) https://www.compart.com/en/unicode/block/U+2460 - for (var i = 0x2460; i <= 0x24B5; i++) - builder.AddChar((char)i); + // "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('⓪'); + } - builder.AddChar('⓪'); return builder.BuildRangesToArray(); } @@ -356,8 +384,17 @@ public sealed class FontManager : IDisposable } } - Ranges = BuildRange(customChars.Count > 0 ? customChars : null, ranges.ToArray()); - JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges); + Ranges = BuildRange( + customChars.Count > 0 ? customChars : null, + includeCommonExtras: true, + ranges.ToArray() + ); + JpRange = BuildRange(GlyphRangesJapanese.GlyphRanges, includeCommonExtras: true); + + // B1: the fallback gets only the trimmed Hangul/Simplified-Han remainder. + // No Default block, no endonyms — those are already merged by the global and + // Japanese fonts, so re-merging them on the fallback was wasted atlas work. + CjkFallbackGlyphRange = BuildRange(CjkFallbackRange.Pairs, includeCommonExtras: false); } // Add font with fallback to NotoSansCjkRegular if unavailable diff --git a/HellionChat/SelfTests/FontManagerCtorSmokeStep.cs b/HellionChat/SelfTests/FontManagerCtorSmokeStep.cs index 1aaf015..c0e8f8c 100644 --- a/HellionChat/SelfTests/FontManagerCtorSmokeStep.cs +++ b/HellionChat/SelfTests/FontManagerCtorSmokeStep.cs @@ -86,6 +86,49 @@ internal sealed class FontManagerCtorSmokeStep : ISelfTestStep return SelfTestStepResult.Fail; } + // B1: assert the atlas actually finished building all required handles, + // not just that the references are non-null. FontsReady is the observable + // state the trimmed-fallback rebuild must still reach; a half-built atlas + // would pass the null/exception checks above but fail here. + if (!fm.FontsReady) + { + ImGui.Text("FontManager.FontsReady is false (atlas not fully built)."); + SelfTestReport.Append( + Name, + "FAIL", + new[] { "FontsReady is false — atlas not fully built." } + ); + return SelfTestStepResult.Fail; + } + + // Report what was actually verified (Flo's request: don't just show Pass). + // The glyph-range entry counts make the B1 dedup visible — the cjk-fallback + // range is now a small trimmed remainder next to the large primary range. + var counts = fm.GlyphRangeLengths; + var italicState = + fm.ItalicFont is null ? "disabled (null)" + : fm.ItalicFont.Available ? "available" + : "NOT available"; + var path = SelfTestReport.Append( + Name, + "PASS", + new[] + { + $"Axis available: {fm.Axis.Available}", + $"AxisItalic available: {fm.AxisItalic.Available}", + $"FontAwesome available: {fm.FontAwesome.Available}", + $"RegularFont available: {fm.RegularFont.Available}", + $"ItalicFont: {italicState}", + $"FontsReady: {fm.FontsReady}", + $"Glyph-range entries: primary={counts.Ranges}, jp={counts.JpRange}, " + + $"cjk-fallback={counts.CjkFallback} (B1 trimmed)", + $"UseHellionFont={Plugin.Config.UseHellionFont}, ItalicEnabled={Plugin.Config.ItalicEnabled}", + } + ); + ImGui.Text( + $"PASS — FontsReady, ranges primary={counts.Ranges}/jp={counts.JpRange}/" + + $"cjk-fallback={counts.CjkFallback}. Report: {path}" + ); return SelfTestStepResult.Pass; }