diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 3d2b58683..5359861cf 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Dalamud.Game.Text; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Style; using Newtonsoft.Json; using Serilog; @@ -128,6 +129,11 @@ namespace Dalamud.Configuration.Internal /// public float GlobalUiScale { get; set; } = 1.0f; + /// + /// Gets or sets the game font to use for Dalamud UI. + /// + public GameFont DefaultFontFromGame { get; set; } = GameFont.Undefined; + /// /// Gets or sets a value indicating whether or not plugin UI should be hidden. /// diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 8d313a646..537ffa516 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -18,6 +18,7 @@ using Dalamud.Game.Network.Internal; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking.Internal; using Dalamud.Interface; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; @@ -191,6 +192,9 @@ namespace Dalamud Service.Set().Enable(); Log.Information("[T2] IM OK!"); + Service.Set(); + Log.Information("[T2] GFM OK!"); + #pragma warning disable CS0618 // Type or member is obsolete Service.Set(); #pragma warning restore CS0618 // Type or member is obsolete diff --git a/Dalamud/Interface/GameFonts/FdtReader.cs b/Dalamud/Interface/GameFonts/FdtReader.cs new file mode 100644 index 000000000..ceaca8096 --- /dev/null +++ b/Dalamud/Interface/GameFonts/FdtReader.cs @@ -0,0 +1,428 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Interface.GameFonts +{ + /// + /// Parses a game font file. + /// + public class FdtReader + { + /// + /// Initializes a new instance of the class. + /// + /// Content of a FDT file. + public FdtReader(byte[] data) + { + unsafe + { + fixed (byte* ptr = data) + { + this.FileHeader = *(FdtHeader*)ptr; + this.FontHeader = *(FontTableHeader*)(ptr + this.FileHeader.FontTableHeaderOffset); + this.KerningHeader = *(KerningTableHeader*)(ptr + this.FileHeader.KerningTableHeaderOffset); + + var glyphs = (FontTableEntry*)(ptr + this.FileHeader.FontTableHeaderOffset + Marshal.SizeOf(this.FontHeader)); + for (var i = 0; i < this.FontHeader.FontTableEntryCount; i++) + this.Glyphs.Add(glyphs[i]); + + var kerns = (KerningTableEntry*)(ptr + this.FileHeader.KerningTableHeaderOffset + Marshal.SizeOf(this.KerningHeader)); + for (var i = 0; i < this.FontHeader.FontTableEntryCount; i++) + this.Distances.Add(kerns[i]); + } + } + } + + /// + /// Gets the header of this file. + /// + public FdtHeader FileHeader { get; init; } + + /// + /// Gets the font header of this file. + /// + public FontTableHeader FontHeader { get; init; } + + /// + /// Gets the kerning table header of this file. + /// + public KerningTableHeader KerningHeader { get; init; } + + /// + /// Gets all the glyphs defined in this file. + /// + public List Glyphs { get; init; } = new(); + + /// + /// Gets all the kerning entries defined in this file. + /// + public List Distances { get; init; } = new(); + + /// + /// Finds glyph definition for corresponding codepoint. + /// + /// Unicode codepoint (UTF-32 value). + /// Corresponding FontTableEntry, or null if not found. + public FontTableEntry? FindGlyph(int codepoint) + { + var i = this.Glyphs.BinarySearch(new FontTableEntry { CharUtf8 = CodePointToUtf8int32(codepoint) }); + if (i < 0 || i == this.Glyphs.Count) + return null; + return this.Glyphs[i]; + } + + /// + /// Returns glyph definition for corresponding codepoint. + /// + /// Unicode codepoint (UTF-32 value). + /// Corresponding FontTableEntry, or that of a fallback character. + public FontTableEntry GetGlyph(int codepoint) + { + return (this.FindGlyph(codepoint) + ?? this.FindGlyph('〓') + ?? this.FindGlyph('?') + ?? this.FindGlyph('='))!.Value; + } + + /// + /// Returns distance adjustment between two adjacent characters. + /// + /// Left character. + /// Right character. + /// Supposed distance adjustment between given characters. + public int GetDistance(int codepoint1, int codepoint2) + { + var i = this.Distances.BinarySearch(new KerningTableEntry { LeftUtf8 = CodePointToUtf8int32(codepoint1), RightUtf8 = CodePointToUtf8int32(codepoint2) }); + if (i < 0 || i == this.Distances.Count) + return 0; + return this.Distances[i].RightOffset; + } + + private static int CodePointToUtf8int32(int codepoint) + { + if (codepoint <= 0x7F) + { + return codepoint; + } + else if (codepoint <= 0x7FF) + { + return ((0xC0 | (codepoint >> 6)) << 8) + | ((0x80 | ((codepoint >> 0) & 0x3F)) << 0); + } + else if (codepoint <= 0xFFFF) + { + return ((0xE0 | (codepoint >> 12)) << 16) + | ((0x80 | ((codepoint >> 6) & 0x3F)) << 8) + | ((0x80 | ((codepoint >> 0) & 0x3F)) << 0); + } + else if (codepoint <= 0x10FFFF) + { + return ((0xF0 | (codepoint >> 18)) << 24) + | ((0x80 | ((codepoint >> 12) & 0x3F)) << 16) + | ((0x80 | ((codepoint >> 6) & 0x3F)) << 8) + | ((0x80 | ((codepoint >> 0) & 0x3F)) << 0); + } + else + { + return 0xFFFE; + } + } + + private static int Utf8Uint32ToCodePoint(int n) + { + if ((n & 0xFFFFFF80) == 0) + { + return n & 0x7F; + } + else if ((n & 0xFFFFE0C0) == 0xC080) + { + return + (((n >> 0x08) & 0x1F) << 6) | + (((n >> 0x00) & 0x3F) << 0); + } + else if ((n & 0xF0C0C0) == 0xE08080) + { + return + (((n >> 0x10) & 0x0F) << 12) | + (((n >> 0x08) & 0x3F) << 6) | + (((n >> 0x00) & 0x3F) << 0); + } + else if ((n & 0xF8C0C0C0) == 0xF0808080) + { + return + (((n >> 0x18) & 0x07) << 18) | + (((n >> 0x10) & 0x3F) << 12) | + (((n >> 0x08) & 0x3F) << 6) | + (((n >> 0x00) & 0x3F) << 0); + } + else + { + return 0xFFFF; // Guaranteed non-unicode + } + } + + /// + /// Header of game font file format. + /// + [StructLayout(LayoutKind.Sequential)] + public unsafe struct FdtHeader + { + /// + /// Signature: "fcsv". + /// + public fixed byte Signature[8]; + + /// + /// Offset to FontTableHeader. + /// + public int FontTableHeaderOffset; + + /// + /// Offset to KerningTableHeader. + /// + public int KerningTableHeaderOffset; + + /// + /// Unused/unknown. + /// + public fixed byte Padding[0x10]; + } + + /// + /// Header of glyph table. + /// + [StructLayout(LayoutKind.Sequential)] + public unsafe struct FontTableHeader + { + /// + /// Signature: "fthd". + /// + public fixed byte Signature[4]; + + /// + /// Number of glyphs defined in this file. + /// + public int FontTableEntryCount; + + /// + /// Number of kerning informations defined in this file. + /// + public int KerningTableEntryCount; + + /// + /// Unused/unknown. + /// + public fixed byte Padding[0x04]; + + /// + /// Width of backing texture. + /// + public ushort TextureWidth; + + /// + /// Height of backing texture. + /// + public ushort TextureHeight; + + /// + /// Size of the font defined from this file, in points unit. + /// + public float Size; + + /// + /// Line height of the font defined forom this file, in pixels unit. + /// + public int LineHeight; + + /// + /// Ascent of the font defined from this file, in pixels unit. + /// + public int Ascent; + + /// + /// Gets descent of the font defined from this file, in pixels unit. + /// + public int Descent => this.LineHeight - this.Ascent; + } + + /// + /// Glyph table entry. + /// + [StructLayout(LayoutKind.Sequential)] + public unsafe struct FontTableEntry : IComparable + { + /// + /// Mapping of texture channel index to byte index. + /// + public static readonly int[] TextureChannelOrder = { 2, 1, 0, 3 }; + + /// + /// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian. + /// + public int CharUtf8; + + /// + /// Integer representation of a Shift_JIS character in reverse order, read in little endian. + /// + public ushort CharSjis; + + /// + /// Index of backing texture. + /// + public ushort TextureIndex; + + /// + /// Horizontal offset of glyph image in the backing texture. + /// + public ushort TextureOffsetX; + + /// + /// Vertical offset of glyph image in the backing texture. + /// + public ushort TextureOffsetY; + + /// + /// Bounding width of this glyph. + /// + public byte BoundingWidth; + + /// + /// Bounding height of this glyph. + /// + public byte BoundingHeight; + + /// + /// Distance adjustment for drawing next character. + /// + public sbyte NextOffsetX; + + /// + /// Distance adjustment for drawing current character. + /// + public sbyte CurrentOffsetY; + + /// + /// Gets the index of the file among all the backing texture files. + /// + public int TextureFileIndex => this.TextureIndex / 4; + + /// + /// Gets the channel index in the backing texture file. + /// + public int TextureChannelIndex => this.TextureIndex % 4; + + /// + /// Gets the byte index in a multichannel pixel corresponding to the channel. + /// + public int TextureChannelByteIndex => TextureChannelOrder[this.TextureChannelIndex]; + + /// + /// Gets the advance width of this character. + /// + public int AdvanceWidth => this.BoundingWidth + this.NextOffsetX; + + /// + /// Gets the Unicode codepoint of the character for this entry in int type. + /// + public int CharInt => Utf8Uint32ToCodePoint(this.CharUtf8); + + /// + /// Gets the Unicode codepoint of the character for this entry in char type. + /// + public char Char => (char)Utf8Uint32ToCodePoint(this.CharUtf8); + + /// + public int CompareTo(FontTableEntry other) + { + return this.CharUtf8 - other.CharUtf8; + } + } + + /// + /// Header of kerning table. + /// + [StructLayout(LayoutKind.Sequential)] + public unsafe struct KerningTableHeader + { + /// + /// Signature: "knhd". + /// + public fixed byte Signature[4]; + + /// + /// Number of kerning entries in this table. + /// + public int Count; + + /// + /// Unused/unknown. + /// + public fixed byte Padding[0x08]; + } + + /// + /// Kerning table entry. + /// + [StructLayout(LayoutKind.Sequential)] + public unsafe struct KerningTableEntry : IComparable + { + /// + /// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian, for the left character. + /// + public int LeftUtf8; + + /// + /// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian, for the right character. + /// + public int RightUtf8; + + /// + /// Integer representation of a Shift_JIS character in reverse order, read in little endian, for the left character. + /// + public ushort LeftSjis; + + /// + /// Integer representation of a Shift_JIS character in reverse order, read in little endian, for the right character. + /// + public ushort RightSjis; + + /// + /// Horizontal offset adjustment for the right character. + /// + public int RightOffset; + + /// + /// Gets the Unicode codepoint of the character for this entry in int type. + /// + public int LeftInt => Utf8Uint32ToCodePoint(this.LeftUtf8); + + /// + /// Gets the Unicode codepoint of the character for this entry in char type. + /// + public char Left => (char)Utf8Uint32ToCodePoint(this.LeftUtf8); + + /// + /// Gets the Unicode codepoint of the character for this entry in int type. + /// + public int RightInt => Utf8Uint32ToCodePoint(this.RightUtf8); + + /// + /// Gets the Unicode codepoint of the character for this entry in char type. + /// + public char Right => (char)Utf8Uint32ToCodePoint(this.RightUtf8); + + /// + public int CompareTo(KerningTableEntry other) + { + if (this.LeftUtf8 == other.LeftUtf8) + return this.RightUtf8 - other.RightUtf8; + else + return this.LeftUtf8 - other.LeftUtf8; + } + } + } +} diff --git a/Dalamud/Interface/GameFonts/GameFont.cs b/Dalamud/Interface/GameFonts/GameFont.cs new file mode 100644 index 000000000..f095b6fcc --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFont.cs @@ -0,0 +1,174 @@ +namespace Dalamud.Interface.GameFonts +{ + /// + /// Enum of available game fonts. + /// + public enum GameFont : int + { + /// + /// Placeholder meaning unused. + /// + Undefined, + + /// + /// AXIS (9.6pt) + /// + /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. + /// + Axis96, + + /// + /// AXIS (12pt) + /// + /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. + /// + Axis12, + + /// + /// AXIS (14pt) + /// + /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. + /// + Axis14, + + /// + /// AXIS (18pt) + /// + /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. + /// + Axis18, + + /// + /// AXIS (36pt) + /// + /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. + /// + Axis36, + + /// + /// Jupiter (16pt) + /// + /// Serif font. Contains mostly ASCII range. Used in game for job names. + /// + Jupiter16, + + /// + /// Jupiter (20pt) + /// + /// Serif font. Contains mostly ASCII range. Used in game for job names. + /// + Jupiter20, + + /// + /// Jupiter (23pt) + /// + /// Serif font. Contains mostly ASCII range. Used in game for job names. + /// + Jupiter23, + + /// + /// Jupiter (45pt) + /// + /// Serif font. Contains mostly numbers. Used in game for flying texts. + /// + Jupiter45, + + /// + /// Jupiter (46pt) + /// + /// Serif font. Contains mostly ASCII range. Used in game for job names. + /// + Jupiter46, + + /// + /// Jupiter (90pt) + /// + /// Serif font. Contains mostly numbers. Used in game for flying texts. + /// + Jupiter90, + + /// + /// Meidinger (16pt) + /// + /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. + /// + Meidinger16, + + /// + /// Meidinger (20pt) + /// + /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. + /// + Meidinger20, + + /// + /// Meidinger (40pt) + /// + /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. + /// + Meidinger40, + + /// + /// MiedingerMid (10pt) + /// + /// Horizontally wide. Contains mostly ASCII range. + /// + MiedingerMid10, + + /// + /// MiedingerMid (12pt) + /// + /// Horizontally wide. Contains mostly ASCII range. + /// + MiedingerMid12, + + /// + /// MiedingerMid (14pt) + /// + /// Horizontally wide. Contains mostly ASCII range. + /// + MiedingerMid14, + + /// + /// MiedingerMid (18pt) + /// + /// Horizontally wide. Contains mostly ASCII range. + /// + MiedingerMid18, + + /// + /// MiedingerMid (36pt) + /// + /// Horizontally wide. Contains mostly ASCII range. + /// + MiedingerMid36, + + /// + /// TrumpGothic (18.4pt) + /// + /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. + /// + TrumpGothic184, + + /// + /// TrumpGothic (23pt) + /// + /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. + /// + TrumpGothic23, + + /// + /// TrumpGothic (34pt) + /// + /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. + /// + TrumpGothic34, + + /// + /// TrumpGothic (688pt) + /// + /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. + /// + TrumpGothic68, + } +} diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs new file mode 100644 index 000000000..6d9274ac0 --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -0,0 +1,35 @@ +using System; + +using ImGuiNET; + +namespace Dalamud.Interface.GameFonts +{ + /// + /// Prepare and keep game font loaded for use in OnDraw. + /// + public class GameFontHandle : IDisposable + { + private readonly GameFontManager manager; + private readonly GameFont font; + + /// + /// Initializes a new instance of the class. + /// + /// GameFontManager instance. + /// Font to use. + internal GameFontHandle(GameFontManager manager, GameFont font) + { + this.manager = manager; + this.font = font; + } + + /// + /// Gets the font. + /// + /// Corresponding font or null. + public ImFontPtr? Get() => this.manager.GetFont(this.font); + + /// + public void Dispose() => this.manager.DecreaseFontRef(this.font); + } +} diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs new file mode 100644 index 000000000..9cca00236 --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -0,0 +1,407 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; + +using Dalamud.Data; +using Dalamud.Interface.Internal; +using ImGuiNET; +using Lumina.Data.Files; +using Serilog; + +namespace Dalamud.Interface.GameFonts +{ + /// + /// Loads game font for use in ImGui. + /// + internal class GameFontManager : IDisposable + { + private static readonly string[] FontNames = + { + null, + "AXIS_96", "AXIS_12", "AXIS_14", "AXIS_18", "AXIS_36", + "Jupiter_16", "Jupiter_20", "Jupiter_23", "Jupiter_45", "Jupiter_46", "Jupiter_90", + "Meidinger_16", "Meidinger_20", "Meidinger_40", + "MiedingerMid_10", "MiedingerMid_12", "MiedingerMid_14", "MiedingerMid_18", "MiedingerMid_36", + "TrumpGothic_184", "TrumpGothic_23", "TrumpGothic_34", "TrumpGothic_68", + }; + + private readonly object syncRoot = new(); + + private readonly InterfaceManager interfaceManager; + + private readonly FdtReader?[] fdts; + private readonly List texturePixels; + private readonly ImFontPtr?[] fonts = new ImFontPtr?[FontNames.Length]; + + private readonly int[] fontUseCounter = new int[FontNames.Length]; + private readonly List>> glyphRectIds = new(); + + /// + /// Initializes a new instance of the class. + /// + public GameFontManager() + { + var dataManager = Service.Get(); + + this.fdts = FontNames.Select(fontName => + { + var file = fontName == null ? null : dataManager.GetFile($"common/font/{fontName}.fdt"); + return file == null ? null : new FdtReader(file!.Data); + }).ToArray(); + this.texturePixels = Enumerable.Range(1, 1 + this.fdts.Where(x => x != null).Select(x => x.Glyphs.Select(x => x.TextureFileIndex).Max()).Max()).Select(x => dataManager.GameData.GetFile($"common/font/font{x}.tex").ImageData).ToList(); + + this.interfaceManager = Service.Get(); + } + + /// + /// Describe font into a string. + /// + /// Font to describe. + /// A string in a form of "FontName (NNNpt)". + public static string DescribeFont(GameFont font) + { + return font switch + { + GameFont.Undefined => "-", + GameFont.Axis96 => "AXIS (9.6pt)", + GameFont.Axis12 => "AXIS (12pt)", + GameFont.Axis14 => "AXIS (14pt)", + GameFont.Axis18 => "AXIS (18pt)", + GameFont.Axis36 => "AXIS (36pt)", + GameFont.Jupiter16 => "Jupiter (16pt)", + GameFont.Jupiter20 => "Jupiter (20pt)", + GameFont.Jupiter23 => "Jupiter (23pt)", + GameFont.Jupiter45 => "Jupiter Numeric (45pt)", + GameFont.Jupiter46 => "Jupiter (46pt)", + GameFont.Jupiter90 => "Jupiter Numeric (90pt)", + GameFont.Meidinger16 => "Meidinger Numeric (16pt)", + GameFont.Meidinger20 => "Meidinger Numeric (20pt)", + GameFont.Meidinger40 => "Meidinger Numeric (40pt)", + GameFont.MiedingerMid10 => "MiedingerMid (10pt)", + GameFont.MiedingerMid12 => "MiedingerMid (12pt)", + GameFont.MiedingerMid14 => "MiedingerMid (14pt)", + GameFont.MiedingerMid18 => "MiedingerMid (18pt)", + GameFont.MiedingerMid36 => "MiedingerMid (36pt)", + GameFont.TrumpGothic184 => "Trump Gothic (18.4pt)", + GameFont.TrumpGothic23 => "Trump Gothic (23pt)", + GameFont.TrumpGothic34 => "Trump Gothic (34pt)", + GameFont.TrumpGothic68 => "Trump Gothic (68pt)", + _ => throw new ArgumentOutOfRangeException(nameof(font), font, "Invalid argument"), + }; + } + + /// + /// Determines whether a font should be able to display most of stuff. + /// + /// Font to check. + /// True if it can. + public static bool IsGenericPurposeFont(GameFont font) + { + return font switch + { + GameFont.Axis96 => true, + GameFont.Axis12 => true, + GameFont.Axis14 => true, + GameFont.Axis18 => true, + GameFont.Axis36 => true, + _ => false, + }; + } + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + public static void CopyGlyphsAcrossFonts(ImFontPtr? source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) + { + if (!source.HasValue || !target.HasValue) + return; + + unsafe + { + var glyphs = (ImFontGlyphReal*)source.Value!.Glyphs.Data; + for (int j = 0, j_ = source.Value!.Glyphs.Size; j < j_; j++) + { + var glyph = &glyphs[j]; + if (glyph->Codepoint < 32 || glyph->Codepoint >= 0xFFFF) + continue; + + var prevGlyphPtr = (ImFontGlyphReal*)target.Value!.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr; + if ((IntPtr)prevGlyphPtr == IntPtr.Zero) + { + target.Value!.AddGlyph( + target.Value!.ConfigData, + (ushort)glyph->Codepoint, + glyph->X0, + glyph->Y0, + glyph->X0 + ((glyph->X1 - glyph->X0) * target.Value!.FontSize / source.Value!.FontSize), + glyph->Y0 + ((glyph->Y1 - glyph->Y0) * target.Value!.FontSize / source.Value!.FontSize), + glyph->U0, + glyph->V0, + glyph->U1, + glyph->V1, + glyph->AdvanceX * target.Value!.FontSize / source.Value!.FontSize); + } + else if (!missingOnly) + { + prevGlyphPtr->X0 = glyph->X0; + prevGlyphPtr->Y0 = glyph->Y0; + prevGlyphPtr->X1 = glyph->X0 + ((glyph->X1 - glyph->X0) * target.Value!.FontSize / source.Value!.FontSize); + prevGlyphPtr->Y1 = glyph->Y0 + ((glyph->Y1 - glyph->Y0) * target.Value!.FontSize / source.Value!.FontSize); + prevGlyphPtr->U0 = glyph->U0; + prevGlyphPtr->V0 = glyph->V0; + prevGlyphPtr->U1 = glyph->U1; + prevGlyphPtr->V1 = glyph->V1; + prevGlyphPtr->AdvanceX = glyph->AdvanceX * target.Value!.FontSize / source.Value!.FontSize; + } + } + } + + if (rebuildLookupTable) + target.Value!.BuildLookupTable(); + } + + /// + public void Dispose() + { + } + + /// + /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. + /// + /// Font to use. + /// Handle to game font that may or may not be ready yet. + public GameFontHandle NewFontRef(GameFont gameFont) + { + var fontIndex = (int)gameFont; + var needRebuild = false; + + lock (this.syncRoot) + { + var prev = this.fontUseCounter[fontIndex] == 0; + this.fontUseCounter[fontIndex] += 1; + needRebuild = prev != (this.fontUseCounter[fontIndex] == 0); + } + + if (needRebuild) + this.interfaceManager.RebuildFonts(); + + return new(this, gameFont); + } + + /// + /// Gets the font. + /// + /// Font to get. + /// Corresponding font or null. + public ImFontPtr? GetFont(GameFont gameFont) => this.fonts[(int)gameFont]; + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFont target, bool missingOnly, bool rebuildLookupTable) + { + GameFontManager.CopyGlyphsAcrossFonts(source, this.fonts[(int)target], missingOnly, rebuildLookupTable); + } + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + public void CopyGlyphsAcrossFonts(GameFont source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) + { + GameFontManager.CopyGlyphsAcrossFonts(this.fonts[(int)source], target, missingOnly, rebuildLookupTable); + } + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + public void CopyGlyphsAcrossFonts(GameFont source, GameFont target, bool missingOnly, bool rebuildLookupTable) + { + GameFontManager.CopyGlyphsAcrossFonts(this.fonts[(int)source], this.fonts[(int)target], missingOnly, rebuildLookupTable); + } + + /// + /// Build fonts before plugins do something more. To be called from InterfaceManager. + /// + public void BuildFonts() + { + this.glyphRectIds.Clear(); + var io = ImGui.GetIO(); + io.Fonts.TexDesiredWidth = 4096; + + for (var i = 0; i < FontNames.Length; i++) + { + this.fonts[i] = null; + this.glyphRectIds.Add(new()); + + var fdt = this.fdts[i]; + if (this.fontUseCounter[i] == 0 || fdt == null) + continue; + + Log.Information($"GameFontManager BuildFont: {FontNames[i]}"); + + var font = io.Fonts.AddFontDefault(); + this.fonts[i] = font; + foreach (var glyph in fdt.Glyphs) + { + var c = glyph.Char; + if (c < 32 || c >= 0xFFFF) + continue; + + this.glyphRectIds[i][c] = Tuple.Create(io.Fonts.AddCustomRectFontGlyph(font, c, glyph.BoundingWidth + 1, glyph.BoundingHeight + 1, glyph.BoundingWidth + glyph.NextOffsetX, new Vector2(0, glyph.CurrentOffsetY)), glyph); + } + } + } + + /// + /// Post-build fonts before plugins do something more. To be called from InterfaceManager. + /// + public unsafe void AfterBuildFonts() + { + var io = ImGui.GetIO(); + io.Fonts.GetTexDataAsRGBA32(out byte* pixels8, out var width, out var height); + var pixels32 = (uint*)pixels8; + + for (var i = 0; i < this.fonts.Length; i++) + { + if (!this.fonts[i].HasValue) + continue; + + var font = this.fonts[i]!.Value; + var fdt = this.fdts[i]; + var fontPtr = font.NativePtr; + fontPtr->ConfigData->SizePixels = fontPtr->FontSize = fdt.FontHeader.LineHeight; + fontPtr->Ascent = fdt.FontHeader.Ascent; + fontPtr->Descent = fdt.FontHeader.Descent; + fontPtr->EllipsisChar = '…'; + foreach (var fallbackCharCandidate in "〓?!") + { + var glyph = font.FindGlyphNoFallback(fallbackCharCandidate); + if ((IntPtr)glyph.NativePtr != IntPtr.Zero) + { + font.SetFallbackChar(fallbackCharCandidate); + break; + } + } + + fixed (char* c = FontNames[i]) + { + for (var j = 0; j < 40; j++) + fontPtr->ConfigData->Name[j] = 0; + Encoding.UTF8.GetBytes(c, FontNames[i].Length, fontPtr->ConfigData->Name, 40); + } + + foreach (var (c, (rectId, glyph)) in this.glyphRectIds[i]) + { + var rc = io.Fonts.GetCustomRectByIndex(rectId); + var sourceBuffer = this.texturePixels[glyph.TextureFileIndex]; + var sourceBufferDelta = glyph.TextureChannelByteIndex; + for (var y = 0; y < glyph.BoundingHeight; y++) + { + for (var x = 0; x < glyph.BoundingWidth; x++) + { + var a = sourceBuffer[sourceBufferDelta + (4 * (((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x))]; + pixels32[((rc.Y + y) * width) + rc.X + x] = (uint)(a << 24) | 0xFFFFFFu; + } + } + } + } + + this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis96, true, false); + this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis12, true, false); + this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis14, true, false); + this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis18, true, false); + this.CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, GameFont.Axis36, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.Jupiter16, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter20, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter23, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter45, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter46, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Jupiter90, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.Meidinger16, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Meidinger20, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.Meidinger40, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis96, GameFont.MiedingerMid10, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis12, GameFont.MiedingerMid12, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis14, GameFont.MiedingerMid14, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.MiedingerMid18, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.MiedingerMid36, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis18, GameFont.TrumpGothic184, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.TrumpGothic23, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.TrumpGothic34, true, false); + this.CopyGlyphsAcrossFonts(GameFont.Axis36, GameFont.TrumpGothic68, true, false); + + foreach (var font in this.fonts) + font?.BuildLookupTable(); + } + + /// + /// Decrease font reference counter and release if nobody is using it. + /// + /// Font to release. + internal void DecreaseFontRef(GameFont gameFont) + { + var fontIndex = (int)gameFont; + var needRebuild = false; + + lock (this.syncRoot) + { + var prev = this.fontUseCounter[fontIndex] == 0; + this.fontUseCounter[fontIndex] -= 1; + needRebuild = prev != (this.fontUseCounter[fontIndex] == 0); + } + + if (needRebuild) + this.interfaceManager.RebuildFonts(); + } + + private struct ImFontGlyphReal + { + public uint ColoredVisibleCodepoint; + public float AdvanceX; + public float X0; + public float Y0; + public float X1; + public float Y1; + public float U0; + public float V0; + public float U1; + public float V1; + + public bool Colored + { + get => ((this.ColoredVisibleCodepoint >> 0) & 1) != 0; + set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFEu) | (value ? 1u : 0u); + } + + public bool Visible + { + get => ((this.ColoredVisibleCodepoint >> 1) & 1) != 0; + set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 0xFFFFFFFDu) | (value ? 2u : 0u); + } + + public int Codepoint + { + get => (int)(this.ColoredVisibleCodepoint >> 2); + set => this.ColoredVisibleCodepoint = (this.ColoredVisibleCodepoint & 3u) | ((uint)this.Codepoint << 2); + } + } + } +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index b285c60ed..1e9654dc0 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -16,6 +16,7 @@ using Dalamud.Game.Gui.Internal; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; using Dalamud.Hooking.Internal; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Windows.StyleEditor; @@ -56,6 +57,9 @@ namespace Dalamud.Interface.Internal private readonly SwapChainVtableResolver address; private RawDX11Scene? scene; + private GameFont overwriteDefaultFontFromGameFont = GameFont.Undefined; + private GameFontHandle? overwriteDefaultFontFromGameFontHandle; + // can't access imgui IO before first present call private bool lastWantCapture = false; private bool isRebuildingFonts = false; @@ -128,10 +132,15 @@ namespace Dalamud.Interface.Internal public event Action ResizeBuffers; /// - /// Gets or sets an action that is executed when fonts are rebuilt. + /// Gets or sets an action that is executed right before fonts are rebuilt. /// public event Action BuildFonts; + /// + /// Gets or sets an action that is executed right after fonts are rebuilt. + /// + public event Action AfterBuildFonts; + /// /// Gets the default ImGui font. /// @@ -304,6 +313,16 @@ namespace Dalamud.Interface.Internal if (!this.isRebuildingFonts) { Log.Verbose("[FONT] RebuildFonts() trigger"); + var configuration = Service.Get(); + if (this.overwriteDefaultFontFromGameFont != configuration.DefaultFontFromGame) + { + this.overwriteDefaultFontFromGameFont = configuration.DefaultFontFromGame; + this.overwriteDefaultFontFromGameFontHandle?.Dispose(); + if (configuration.DefaultFontFromGame == GameFont.Undefined) + this.overwriteDefaultFontFromGameFontHandle = null; + else + this.overwriteDefaultFontFromGameFontHandle = Service.Get().NewFontRef(configuration.DefaultFontFromGame); + } this.isRebuildingFonts = true; this.scene.OnNewRenderFrame += this.RebuildFontsInternal; @@ -384,6 +403,16 @@ namespace Dalamud.Interface.Internal this.scene.OnBuildUI += this.Display; this.scene.OnNewInputFrame += this.OnNewInputFrame; + if (this.overwriteDefaultFontFromGameFont != configuration.DefaultFontFromGame) + { + this.overwriteDefaultFontFromGameFont = configuration.DefaultFontFromGame; + this.overwriteDefaultFontFromGameFontHandle?.Dispose(); + if (configuration.DefaultFontFromGame == GameFont.Undefined) + this.overwriteDefaultFontFromGameFontHandle = null; + else + this.overwriteDefaultFontFromGameFontHandle = Service.Get().NewFontRef(configuration.DefaultFontFromGame); + } + this.SetupFonts(); StyleModel.TransferOldModels(); @@ -496,7 +525,6 @@ namespace Dalamud.Interface.Internal ImGui.GetIO().Fonts.Clear(); ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig.MergeMode = true; fontConfig.PixelSnapH = true; var fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); @@ -522,7 +550,9 @@ namespace Dalamud.Interface.Internal }, GCHandleType.Pinned); + fontConfig.MergeMode = false; ImGui.GetIO().Fonts.AddFontFromFileTTF(fontPathGame, 17.0f, fontConfig, gameRangeHandle.AddrOfPinnedObject()); + fontConfig.MergeMode = true; var fontPathIcon = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "FontAwesome5FreeSolid.otf"); @@ -546,6 +576,9 @@ namespace Dalamud.Interface.Internal MonoFont = ImGui.GetIO().Fonts.AddFontFromFileTTF(fontPathMono, 16.0f); + var gameFontManager = Service.Get(); + gameFontManager.BuildFonts(); + Log.Verbose("[FONT] Invoke OnBuildFonts"); this.BuildFonts?.Invoke(); Log.Verbose("[FONT] OnBuildFonts OK!"); @@ -557,6 +590,13 @@ namespace Dalamud.Interface.Internal ImGui.GetIO().Fonts.Build(); + gameFontManager.AfterBuildFonts(); + GameFontManager.CopyGlyphsAcrossFonts(this.overwriteDefaultFontFromGameFontHandle?.Get(), DefaultFont, false, true); + + Log.Verbose("[FONT] Invoke OnAfterBuildFonts"); + this.AfterBuildFonts?.Invoke(); + Log.Verbose("[FONT] OnAfterBuildFonts OK!"); + Log.Verbose("[FONT] Fonts built!"); this.fontBuildSignal.Set(); diff --git a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs index d02dae56d..6fa6df808 100644 --- a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs @@ -12,6 +12,7 @@ using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Text; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; using Dalamud.Utility; @@ -27,6 +28,8 @@ namespace Dalamud.Interface.Internal.Windows private const float MinScale = 0.3f; private const float MaxScale = 2.0f; + private readonly List validFontChoices; + private readonly string[] validFontNames; private readonly string[] languages; private readonly string[] locLanguages; private int langIndex; @@ -66,6 +69,8 @@ namespace Dalamud.Interface.Internal.Windows private bool doButtonsSystemMenu; private bool disableRmtFiltering; + private int validFontIndex; + #region Experimental private bool doPluginTest; @@ -111,6 +116,10 @@ namespace Dalamud.Interface.Internal.Windows this.doButtonsSystemMenu = configuration.DoButtonsSystemMenu; this.disableRmtFiltering = configuration.DisableRmtFiltering; + this.validFontChoices = Enum.GetValues().Where(x => x == GameFont.Undefined || GameFontManager.IsGenericPurposeFont(x)).ToList(); + this.validFontNames = this.validFontChoices.Select(x => GameFontManager.DescribeFont(x)).ToArray(); + this.validFontIndex = Math.Max(0, this.validFontChoices.IndexOf(configuration.DefaultFontFromGame)); + this.languages = Localization.ApplicableLangCodes.Prepend("en").ToArray(); try { @@ -286,6 +295,11 @@ namespace Dalamud.Interface.Internal.Windows ImGuiHelpers.ScaledDummy(10, 16); + ImGui.Text(Loc.Localize("DalamudSettingsGlobalFont", "Global Font")); + ImGui.Combo("##DalamudSettingsGlobalFontDrag", ref this.validFontIndex, this.validFontNames, this.validFontNames.Length); + + ImGuiHelpers.ScaledDummy(10, 16); + if (ImGui.Button(Loc.Localize("DalamudSettingsOpenStyleEditor", "Open Style Editor"))) { Service.Get().OpenStyleEditor(); @@ -800,6 +814,8 @@ namespace Dalamud.Interface.Internal.Windows configuration.IsFocusManagementEnabled = this.doFocus; configuration.ShowTsm = this.doTsm; + configuration.DefaultFontFromGame = this.validFontChoices[this.validFontIndex]; + // This is applied every frame in InterfaceManager::CheckViewportState() configuration.IsDisableViewport = !this.doViewport; @@ -842,6 +858,8 @@ namespace Dalamud.Interface.Internal.Windows configuration.Save(); + Service.Get().RebuildFonts(); + _ = Service.Get().ReloadPluginMastersAsync(); } } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 445ac3618..35c82102b 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; @@ -39,6 +40,7 @@ namespace Dalamud.Interface var interfaceManager = Service.Get(); interfaceManager.Draw += this.OnDraw; interfaceManager.BuildFonts += this.OnBuildFonts; + interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts; interfaceManager.ResizeBuffers += this.OnResizeBuffers; } @@ -67,6 +69,15 @@ namespace Dalamud.Interface /// public event Action BuildFonts; + /// + /// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.
+ /// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt + /// (at any time), so you should both reload your custom fonts and restore those + /// pointers inside this handler.
+ /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! + ///
+ public event Action AfterBuildFonts; + /// /// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons. /// @@ -201,6 +212,13 @@ namespace Dalamud.Interface public TextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels) => Service.Get().LoadImageRaw(imageData, width, height, numChannels); + /// + /// Gets a game font. + /// + /// Font to get. + /// Handle to the game font which may or may not be available for use yet. + public GameFontHandle GetGameFontHandle(GameFont gameFont) => Service.Get().NewFontRef(gameFont); + /// /// Call this to queue a rebuild of the font atlas.
/// This will invoke any handlers and ensure that any loaded fonts are @@ -320,6 +338,11 @@ namespace Dalamud.Interface this.BuildFonts?.Invoke(); } + private void OnAfterBuildFonts() + { + this.AfterBuildFonts?.Invoke(); + } + private void OnResizeBuffers() { this.ResizeBuffers?.Invoke();