Implement feature to use game resource fonts

This commit is contained in:
Soreepeong 2022-02-24 18:56:34 +09:00
parent b7c47b4c97
commit f3588dfe23
9 changed files with 1137 additions and 2 deletions

View file

@ -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
/// </summary>
public float GlobalUiScale { get; set; } = 1.0f;
/// <summary>
/// Gets or sets the game font to use for Dalamud UI.
/// </summary>
public GameFont DefaultFontFromGame { get; set; } = GameFont.Undefined;
/// <summary>
/// Gets or sets a value indicating whether or not plugin UI should be hidden.
/// </summary>

View file

@ -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<InterfaceManager>.Set().Enable();
Log.Information("[T2] IM OK!");
Service<GameFontManager>.Set();
Log.Information("[T2] GFM OK!");
#pragma warning disable CS0618 // Type or member is obsolete
Service<SeStringManager>.Set();
#pragma warning restore CS0618 // Type or member is obsolete

View file

@ -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
{
/// <summary>
/// Parses a game font file.
/// </summary>
public class FdtReader
{
/// <summary>
/// Initializes a new instance of the <see cref="FdtReader"/> class.
/// </summary>
/// <param name="data">Content of a FDT file.</param>
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]);
}
}
}
/// <summary>
/// Gets the header of this file.
/// </summary>
public FdtHeader FileHeader { get; init; }
/// <summary>
/// Gets the font header of this file.
/// </summary>
public FontTableHeader FontHeader { get; init; }
/// <summary>
/// Gets the kerning table header of this file.
/// </summary>
public KerningTableHeader KerningHeader { get; init; }
/// <summary>
/// Gets all the glyphs defined in this file.
/// </summary>
public List<FontTableEntry> Glyphs { get; init; } = new();
/// <summary>
/// Gets all the kerning entries defined in this file.
/// </summary>
public List<KerningTableEntry> Distances { get; init; } = new();
/// <summary>
/// Finds glyph definition for corresponding codepoint.
/// </summary>
/// <param name="codepoint">Unicode codepoint (UTF-32 value).</param>
/// <returns>Corresponding FontTableEntry, or null if not found.</returns>
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];
}
/// <summary>
/// Returns glyph definition for corresponding codepoint.
/// </summary>
/// <param name="codepoint">Unicode codepoint (UTF-32 value).</param>
/// <returns>Corresponding FontTableEntry, or that of a fallback character.</returns>
public FontTableEntry GetGlyph(int codepoint)
{
return (this.FindGlyph(codepoint)
?? this.FindGlyph('〓')
?? this.FindGlyph('?')
?? this.FindGlyph('='))!.Value;
}
/// <summary>
/// Returns distance adjustment between two adjacent characters.
/// </summary>
/// <param name="codepoint1">Left character.</param>
/// <param name="codepoint2">Right character.</param>
/// <returns>Supposed distance adjustment between given characters.</returns>
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
}
}
/// <summary>
/// Header of game font file format.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public unsafe struct FdtHeader
{
/// <summary>
/// Signature: "fcsv".
/// </summary>
public fixed byte Signature[8];
/// <summary>
/// Offset to FontTableHeader.
/// </summary>
public int FontTableHeaderOffset;
/// <summary>
/// Offset to KerningTableHeader.
/// </summary>
public int KerningTableHeaderOffset;
/// <summary>
/// Unused/unknown.
/// </summary>
public fixed byte Padding[0x10];
}
/// <summary>
/// Header of glyph table.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public unsafe struct FontTableHeader
{
/// <summary>
/// Signature: "fthd".
/// </summary>
public fixed byte Signature[4];
/// <summary>
/// Number of glyphs defined in this file.
/// </summary>
public int FontTableEntryCount;
/// <summary>
/// Number of kerning informations defined in this file.
/// </summary>
public int KerningTableEntryCount;
/// <summary>
/// Unused/unknown.
/// </summary>
public fixed byte Padding[0x04];
/// <summary>
/// Width of backing texture.
/// </summary>
public ushort TextureWidth;
/// <summary>
/// Height of backing texture.
/// </summary>
public ushort TextureHeight;
/// <summary>
/// Size of the font defined from this file, in points unit.
/// </summary>
public float Size;
/// <summary>
/// Line height of the font defined forom this file, in pixels unit.
/// </summary>
public int LineHeight;
/// <summary>
/// Ascent of the font defined from this file, in pixels unit.
/// </summary>
public int Ascent;
/// <summary>
/// Gets descent of the font defined from this file, in pixels unit.
/// </summary>
public int Descent => this.LineHeight - this.Ascent;
}
/// <summary>
/// Glyph table entry.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public unsafe struct FontTableEntry : IComparable<FontTableEntry>
{
/// <summary>
/// Mapping of texture channel index to byte index.
/// </summary>
public static readonly int[] TextureChannelOrder = { 2, 1, 0, 3 };
/// <summary>
/// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian.
/// </summary>
public int CharUtf8;
/// <summary>
/// Integer representation of a Shift_JIS character in reverse order, read in little endian.
/// </summary>
public ushort CharSjis;
/// <summary>
/// Index of backing texture.
/// </summary>
public ushort TextureIndex;
/// <summary>
/// Horizontal offset of glyph image in the backing texture.
/// </summary>
public ushort TextureOffsetX;
/// <summary>
/// Vertical offset of glyph image in the backing texture.
/// </summary>
public ushort TextureOffsetY;
/// <summary>
/// Bounding width of this glyph.
/// </summary>
public byte BoundingWidth;
/// <summary>
/// Bounding height of this glyph.
/// </summary>
public byte BoundingHeight;
/// <summary>
/// Distance adjustment for drawing next character.
/// </summary>
public sbyte NextOffsetX;
/// <summary>
/// Distance adjustment for drawing current character.
/// </summary>
public sbyte CurrentOffsetY;
/// <summary>
/// Gets the index of the file among all the backing texture files.
/// </summary>
public int TextureFileIndex => this.TextureIndex / 4;
/// <summary>
/// Gets the channel index in the backing texture file.
/// </summary>
public int TextureChannelIndex => this.TextureIndex % 4;
/// <summary>
/// Gets the byte index in a multichannel pixel corresponding to the channel.
/// </summary>
public int TextureChannelByteIndex => TextureChannelOrder[this.TextureChannelIndex];
/// <summary>
/// Gets the advance width of this character.
/// </summary>
public int AdvanceWidth => this.BoundingWidth + this.NextOffsetX;
/// <summary>
/// Gets the Unicode codepoint of the character for this entry in int type.
/// </summary>
public int CharInt => Utf8Uint32ToCodePoint(this.CharUtf8);
/// <summary>
/// Gets the Unicode codepoint of the character for this entry in char type.
/// </summary>
public char Char => (char)Utf8Uint32ToCodePoint(this.CharUtf8);
/// <inheritdoc/>
public int CompareTo(FontTableEntry other)
{
return this.CharUtf8 - other.CharUtf8;
}
}
/// <summary>
/// Header of kerning table.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public unsafe struct KerningTableHeader
{
/// <summary>
/// Signature: "knhd".
/// </summary>
public fixed byte Signature[4];
/// <summary>
/// Number of kerning entries in this table.
/// </summary>
public int Count;
/// <summary>
/// Unused/unknown.
/// </summary>
public fixed byte Padding[0x08];
}
/// <summary>
/// Kerning table entry.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public unsafe struct KerningTableEntry : IComparable<KerningTableEntry>
{
/// <summary>
/// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian, for the left character.
/// </summary>
public int LeftUtf8;
/// <summary>
/// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian, for the right character.
/// </summary>
public int RightUtf8;
/// <summary>
/// Integer representation of a Shift_JIS character in reverse order, read in little endian, for the left character.
/// </summary>
public ushort LeftSjis;
/// <summary>
/// Integer representation of a Shift_JIS character in reverse order, read in little endian, for the right character.
/// </summary>
public ushort RightSjis;
/// <summary>
/// Horizontal offset adjustment for the right character.
/// </summary>
public int RightOffset;
/// <summary>
/// Gets the Unicode codepoint of the character for this entry in int type.
/// </summary>
public int LeftInt => Utf8Uint32ToCodePoint(this.LeftUtf8);
/// <summary>
/// Gets the Unicode codepoint of the character for this entry in char type.
/// </summary>
public char Left => (char)Utf8Uint32ToCodePoint(this.LeftUtf8);
/// <summary>
/// Gets the Unicode codepoint of the character for this entry in int type.
/// </summary>
public int RightInt => Utf8Uint32ToCodePoint(this.RightUtf8);
/// <summary>
/// Gets the Unicode codepoint of the character for this entry in char type.
/// </summary>
public char Right => (char)Utf8Uint32ToCodePoint(this.RightUtf8);
/// <inheritdoc/>
public int CompareTo(KerningTableEntry other)
{
if (this.LeftUtf8 == other.LeftUtf8)
return this.RightUtf8 - other.RightUtf8;
else
return this.LeftUtf8 - other.LeftUtf8;
}
}
}
}

View file

@ -0,0 +1,174 @@
namespace Dalamud.Interface.GameFonts
{
/// <summary>
/// Enum of available game fonts.
/// </summary>
public enum GameFont : int
{
/// <summary>
/// Placeholder meaning unused.
/// </summary>
Undefined,
/// <summary>
/// AXIS (9.6pt)
///
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
/// </summary>
Axis96,
/// <summary>
/// AXIS (12pt)
///
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
/// </summary>
Axis12,
/// <summary>
/// AXIS (14pt)
///
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
/// </summary>
Axis14,
/// <summary>
/// AXIS (18pt)
///
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
/// </summary>
Axis18,
/// <summary>
/// AXIS (36pt)
///
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
/// </summary>
Axis36,
/// <summary>
/// Jupiter (16pt)
///
/// Serif font. Contains mostly ASCII range. Used in game for job names.
/// </summary>
Jupiter16,
/// <summary>
/// Jupiter (20pt)
///
/// Serif font. Contains mostly ASCII range. Used in game for job names.
/// </summary>
Jupiter20,
/// <summary>
/// Jupiter (23pt)
///
/// Serif font. Contains mostly ASCII range. Used in game for job names.
/// </summary>
Jupiter23,
/// <summary>
/// Jupiter (45pt)
///
/// Serif font. Contains mostly numbers. Used in game for flying texts.
/// </summary>
Jupiter45,
/// <summary>
/// Jupiter (46pt)
///
/// Serif font. Contains mostly ASCII range. Used in game for job names.
/// </summary>
Jupiter46,
/// <summary>
/// Jupiter (90pt)
///
/// Serif font. Contains mostly numbers. Used in game for flying texts.
/// </summary>
Jupiter90,
/// <summary>
/// Meidinger (16pt)
///
/// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff.
/// </summary>
Meidinger16,
/// <summary>
/// Meidinger (20pt)
///
/// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff.
/// </summary>
Meidinger20,
/// <summary>
/// Meidinger (40pt)
///
/// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff.
/// </summary>
Meidinger40,
/// <summary>
/// MiedingerMid (10pt)
///
/// Horizontally wide. Contains mostly ASCII range.
/// </summary>
MiedingerMid10,
/// <summary>
/// MiedingerMid (12pt)
///
/// Horizontally wide. Contains mostly ASCII range.
/// </summary>
MiedingerMid12,
/// <summary>
/// MiedingerMid (14pt)
///
/// Horizontally wide. Contains mostly ASCII range.
/// </summary>
MiedingerMid14,
/// <summary>
/// MiedingerMid (18pt)
///
/// Horizontally wide. Contains mostly ASCII range.
/// </summary>
MiedingerMid18,
/// <summary>
/// MiedingerMid (36pt)
///
/// Horizontally wide. Contains mostly ASCII range.
/// </summary>
MiedingerMid36,
/// <summary>
/// TrumpGothic (18.4pt)
///
/// Horizontally narrow. Contains mostly ASCII range. Used for addon titles.
/// </summary>
TrumpGothic184,
/// <summary>
/// TrumpGothic (23pt)
///
/// Horizontally narrow. Contains mostly ASCII range. Used for addon titles.
/// </summary>
TrumpGothic23,
/// <summary>
/// TrumpGothic (34pt)
///
/// Horizontally narrow. Contains mostly ASCII range. Used for addon titles.
/// </summary>
TrumpGothic34,
/// <summary>
/// TrumpGothic (688pt)
///
/// Horizontally narrow. Contains mostly ASCII range. Used for addon titles.
/// </summary>
TrumpGothic68,
}
}

View file

@ -0,0 +1,35 @@
using System;
using ImGuiNET;
namespace Dalamud.Interface.GameFonts
{
/// <summary>
/// Prepare and keep game font loaded for use in OnDraw.
/// </summary>
public class GameFontHandle : IDisposable
{
private readonly GameFontManager manager;
private readonly GameFont font;
/// <summary>
/// Initializes a new instance of the <see cref="GameFontHandle"/> class.
/// </summary>
/// <param name="manager">GameFontManager instance.</param>
/// <param name="font">Font to use.</param>
internal GameFontHandle(GameFontManager manager, GameFont font)
{
this.manager = manager;
this.font = font;
}
/// <summary>
/// Gets the font.
/// </summary>
/// <returns>Corresponding font or null.</returns>
public ImFontPtr? Get() => this.manager.GetFont(this.font);
/// <inheritdoc/>
public void Dispose() => this.manager.DecreaseFontRef(this.font);
}
}

View file

@ -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
{
/// <summary>
/// Loads game font for use in ImGui.
/// </summary>
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<byte[]> texturePixels;
private readonly ImFontPtr?[] fonts = new ImFontPtr?[FontNames.Length];
private readonly int[] fontUseCounter = new int[FontNames.Length];
private readonly List<Dictionary<char, Tuple<int, FdtReader.FontTableEntry>>> glyphRectIds = new();
/// <summary>
/// Initializes a new instance of the <see cref="GameFontManager"/> class.
/// </summary>
public GameFontManager()
{
var dataManager = Service<DataManager>.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<TexFile>($"common/font/font{x}.tex").ImageData).ToList();
this.interfaceManager = Service<InterfaceManager>.Get();
}
/// <summary>
/// Describe font into a string.
/// </summary>
/// <param name="font">Font to describe.</param>
/// <returns>A string in a form of "FontName (NNNpt)".</returns>
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"),
};
}
/// <summary>
/// Determines whether a font should be able to display most of stuff.
/// </summary>
/// <param name="font">Font to check.</param>
/// <returns>True if it can.</returns>
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,
};
}
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
/// <param name="source">Source font.</param>
/// <param name="target">Target font.</param>
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
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();
}
/// <inheritdoc/>
public void Dispose()
{
}
/// <summary>
/// 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.
/// </summary>
/// <param name="gameFont">Font to use.</param>
/// <returns>Handle to game font that may or may not be ready yet.</returns>
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);
}
/// <summary>
/// Gets the font.
/// </summary>
/// <param name="gameFont">Font to get.</param>
/// <returns>Corresponding font or null.</returns>
public ImFontPtr? GetFont(GameFont gameFont) => this.fonts[(int)gameFont];
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
/// <param name="source">Source font.</param>
/// <param name="target">Target font.</param>
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFont target, bool missingOnly, bool rebuildLookupTable)
{
GameFontManager.CopyGlyphsAcrossFonts(source, this.fonts[(int)target], missingOnly, rebuildLookupTable);
}
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
/// <param name="source">Source font.</param>
/// <param name="target">Target font.</param>
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
public void CopyGlyphsAcrossFonts(GameFont source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable)
{
GameFontManager.CopyGlyphsAcrossFonts(this.fonts[(int)source], target, missingOnly, rebuildLookupTable);
}
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
/// <param name="source">Source font.</param>
/// <param name="target">Target font.</param>
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
public void CopyGlyphsAcrossFonts(GameFont source, GameFont target, bool missingOnly, bool rebuildLookupTable)
{
GameFontManager.CopyGlyphsAcrossFonts(this.fonts[(int)source], this.fonts[(int)target], missingOnly, rebuildLookupTable);
}
/// <summary>
/// Build fonts before plugins do something more. To be called from InterfaceManager.
/// </summary>
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);
}
}
}
/// <summary>
/// Post-build fonts before plugins do something more. To be called from InterfaceManager.
/// </summary>
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();
}
/// <summary>
/// Decrease font reference counter and release if nobody is using it.
/// </summary>
/// <param name="gameFont">Font to release.</param>
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);
}
}
}
}

View file

@ -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;
/// <summary>
/// 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.
/// </summary>
public event Action BuildFonts;
/// <summary>
/// Gets or sets an action that is executed right after fonts are rebuilt.
/// </summary>
public event Action AfterBuildFonts;
/// <summary>
/// Gets the default ImGui font.
/// </summary>
@ -304,6 +313,16 @@ namespace Dalamud.Interface.Internal
if (!this.isRebuildingFonts)
{
Log.Verbose("[FONT] RebuildFonts() trigger");
var configuration = Service<DalamudConfiguration>.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<GameFontManager>.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<GameFontManager>.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<GameFontManager>.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();

View file

@ -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<GameFont> 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<GameFont>().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<DalamudInterface>.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<InterfaceManager>.Get().RebuildFonts();
_ = Service<PluginManager>.Get().ReloadPluginMastersAsync();
}
}

View file

@ -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<InterfaceManager>.Get();
interfaceManager.Draw += this.OnDraw;
interfaceManager.BuildFonts += this.OnBuildFonts;
interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts;
interfaceManager.ResizeBuffers += this.OnResizeBuffers;
}
@ -67,6 +69,15 @@ namespace Dalamud.Interface
/// </summary>
public event Action BuildFonts;
/// <summary>
/// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.<br/>
/// Any ImFontPtr objects that you store <strong>can be invalidated</strong> when fonts are rebuilt
/// (at any time), so you should both reload your custom fonts and restore those
/// pointers inside this handler.<br/>
/// <strong>PLEASE remove this handler inside Dispose, or when you no longer need your fonts!</strong>
/// </summary>
public event Action AfterBuildFonts;
/// <summary>
/// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons.
/// </summary>
@ -201,6 +212,13 @@ namespace Dalamud.Interface
public TextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels)
=> Service<InterfaceManager>.Get().LoadImageRaw(imageData, width, height, numChannels);
/// <summary>
/// Gets a game font.
/// </summary>
/// <param name="gameFont">Font to get.</param>
/// <returns>Handle to the game font which may or may not be available for use yet.</returns>
public GameFontHandle GetGameFontHandle(GameFont gameFont) => Service<GameFontManager>.Get().NewFontRef(gameFont);
/// <summary>
/// Call this to queue a rebuild of the font atlas.<br/>
/// This will invoke any <see cref="OnBuildFonts"/> 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();