diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d9c1af8f4..70c0b33ea 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -5,7 +5,7 @@ concurrency: build_dalamud
jobs:
build:
name: Build on Windows
- runs-on: windows-2019
+ runs-on: windows-2022
steps:
- name: Checkout Dalamud
uses: actions/checkout@v2
diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj
index f1ce7bac9..3b7556af7 100644
--- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj
+++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj
@@ -3,7 +3,7 @@
Dalamud.CorePlugin
net5.0-windows
x64
- 9.0
+ 10.0
true
false
false
diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj
index d9ce042a6..dbe4c94da 100644
--- a/Dalamud.Injector/Dalamud.Injector.csproj
+++ b/Dalamud.Injector/Dalamud.Injector.csproj
@@ -5,7 +5,7 @@
win-x64
x64
x64;AnyCPU
- 9.0
+ 10.0
diff --git a/Dalamud/ClientLanguageExtensions.cs b/Dalamud/ClientLanguageExtensions.cs
index dccefb93f..abfba3ad5 100644
--- a/Dalamud/ClientLanguageExtensions.cs
+++ b/Dalamud/ClientLanguageExtensions.cs
@@ -10,8 +10,8 @@ namespace Dalamud
///
/// Converts a Dalamud ClientLanguage to the corresponding Lumina variant.
///
- /// Langauge to convert.
- /// Converted langauge.
+ /// Language to convert.
+ /// Converted language.
public static Lumina.Data.Language ToLumina(this ClientLanguage language)
{
return language switch
diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs
index 4405a11a7..7930f5c79 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 a value indicating whether to use AXIS fonts from the game.
+ ///
+ public bool UseAxisFontsFromGame { get; set; } = false;
+
///
/// Gets or sets a value indicating whether or not plugin UI should be hidden.
///
@@ -261,6 +267,15 @@ namespace Dalamud.Configuration.Internal
///
public int DtrSpacing { get; set; } = 10;
+ ///
+ /// Gets or sets a value indicating whether to swap the
+ /// direction in which elements are drawn in the DTR.
+ /// False indicates that elements will be drawn from the end of
+ /// the left side of the Server Info bar, and continue leftwards.
+ /// True indicates the opposite.
+ ///
+ public bool DtrSwapDirection { get; set; } = false;
+
///
/// Gets or sets a value indicating whether the title screen menu is shown.
///
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/Dalamud.csproj b/Dalamud/Dalamud.csproj
index bd8c971b1..2d2ef88b6 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -4,11 +4,11 @@
net5.0-windows
x64
x64;AnyCPU
- 9.0
+ 10.0
- 6.3.0.2
+ 6.3.0.4
XIV Launcher addon framework
$(DalamudVersion)
$(DalamudVersion)
diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs
index 4a703ff29..bcd100120 100644
--- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs
+++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs
@@ -1,6 +1,8 @@
using System;
using Dalamud.Hooking;
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
using ImGuiNET;
using Serilog;
@@ -11,6 +13,8 @@ namespace Dalamud.Game.ClientState.GamePad
///
/// Will block game's gamepad input if is set.
///
+ [PluginInterface]
+ [InterfaceVersion("1.0.0")]
public unsafe class GamepadState : IDisposable
{
private readonly Hook gamepadPoll;
@@ -32,14 +36,6 @@ namespace Dalamud.Game.ClientState.GamePad
this.gamepadPoll = new Hook(resolver.GamepadPoll, this.GamepadPollDetour);
}
- ///
- /// Finalizes an instance of the class.
- ///
- ~GamepadState()
- {
- this.Dispose(false);
- }
-
private delegate int ControllerPoll(IntPtr controllerInput);
///
@@ -164,14 +160,6 @@ namespace Dalamud.Game.ClientState.GamePad
/// 1 the whole time button is pressed, 0 otherwise.
public float Raw(GamepadButtons button) => (this.ButtonsRaw & (ushort)button) > 0 ? 1 : 0;
- ///
- /// Enables the hook of the GamepadPoll function.
- ///
- public void Enable()
- {
- this.gamepadPoll.Enable();
- }
-
///
/// Disposes this instance, alongside its hooks.
///
@@ -181,6 +169,14 @@ namespace Dalamud.Game.ClientState.GamePad
GC.SuppressFinalize(this);
}
+ ///
+ /// Enables the hook of the GamepadPoll function.
+ ///
+ internal void Enable()
+ {
+ this.gamepadPoll.Enable();
+ }
+
private int GamepadPollDetour(IntPtr gamepadInput)
{
var original = this.gamepadPoll.Original(gamepadInput);
diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs
index f85bf1ddf..fce98c8d0 100644
--- a/Dalamud/Game/Gui/Dtr/DtrBar.cs
+++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs
@@ -19,8 +19,10 @@ namespace Dalamud.Game.Gui.Dtr
[InterfaceVersion("1.0")]
public sealed unsafe class DtrBar : IDisposable
{
+ private const uint BaseNodeId = 1000;
+
private List entries = new();
- private uint runningNodeIds = 1000;
+ private uint runningNodeIds = BaseNodeId;
///
/// Initializes a new instance of the class.
@@ -48,10 +50,14 @@ namespace Dalamud.Game.Gui.Dtr
if (this.entries.Any(x => x.Title == title))
throw new ArgumentException("An entry with the same title already exists.");
+ var configuration = Service.Get();
var node = this.MakeNode(++this.runningNodeIds);
var entry = new DtrBarEntry(title, node);
entry.Text = text;
+ // Add the entry to the end of the order list, if it's not there already.
+ if (!configuration.DtrOrder!.Contains(title))
+ configuration.DtrOrder!.Add(title);
this.entries.Add(entry);
this.ApplySort();
@@ -68,6 +74,19 @@ namespace Dalamud.Game.Gui.Dtr
Service.Get().Update -= this.Update;
}
+ ///
+ /// Remove nodes marked as "should be removed" from the bar.
+ ///
+ internal void HandleRemovedNodes()
+ {
+ foreach (var data in this.entries.Where(d => d.ShouldBeRemoved))
+ {
+ this.RemoveNode(data.TextNode);
+ }
+
+ this.entries.RemoveAll(d => d.ShouldBeRemoved);
+ }
+
///
/// Check whether an entry with the specified title exists.
///
@@ -98,46 +117,44 @@ namespace Dalamud.Game.Gui.Dtr
var configuration = Service.Get();
// Sort the current entry list, based on the order in the configuration.
- var ordered = configuration.DtrOrder.Select(entry => this.entries.FirstOrDefault(x => x.Title == entry)).Where(value => value != null).ToList();
+ var positions = configuration.DtrOrder!
+ .Select(entry => (entry, index: configuration.DtrOrder!.IndexOf(entry)))
+ .ToDictionary(x => x.entry, x => x.index);
- // Add entries that weren't sorted to the end of the list.
- if (ordered.Count != this.entries.Count)
+ this.entries.Sort((x, y) =>
{
- ordered.AddRange(this.entries.Where(x => ordered.All(y => y.Title != x.Title)));
- }
-
- // Update the order list for new entries.
- configuration.DtrOrder.Clear();
- foreach (var dtrEntry in ordered)
- {
- configuration.DtrOrder.Add(dtrEntry.Title);
- }
-
- this.entries = ordered;
+ var xPos = positions.TryGetValue(x.Title, out var xIndex) ? xIndex : int.MaxValue;
+ var yPos = positions.TryGetValue(y.Title, out var yIndex) ? yIndex : int.MaxValue;
+ return xPos.CompareTo(yPos);
+ });
}
private static AtkUnitBase* GetDtr() => (AtkUnitBase*)Service.Get().GetAddonByName("_DTR", 1).ToPointer();
private void Update(Framework unused)
{
+ this.HandleRemovedNodes();
+
var dtr = GetDtr();
if (dtr == null) return;
- foreach (var data in this.entries.Where(d => d.ShouldBeRemoved))
- {
- this.RemoveNode(data.TextNode);
- }
-
- this.entries.RemoveAll(d => d.ShouldBeRemoved);
-
// The collision node on the DTR element is always the width of its content
if (dtr->UldManager.NodeList == null) return;
+
+ // If we have an unmodified DTR but still have entries, we need to
+ // work to reset our state.
+ if (!this.CheckForDalamudNodes())
+ this.RecreateNodes();
+
var collisionNode = dtr->UldManager.NodeList[1];
if (collisionNode == null) return;
- var runningXPos = collisionNode->X;
var configuration = Service.Get();
+ // If we are drawing backwards, we should start from the right side of the collision node. That is,
+ // collisionNode->X + collisionNode->Width.
+ var runningXPos = configuration.DtrSwapDirection ? collisionNode->X + collisionNode->Width : collisionNode->X;
+
for (var i = 0; i < this.entries.Count; i++)
{
var data = this.entries[i];
@@ -170,18 +187,56 @@ namespace Dalamud.Game.Gui.Dtr
if (!isHide)
{
- runningXPos -= data.TextNode->AtkResNode.Width + configuration.DtrSpacing;
- data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
+ var elementWidth = data.TextNode->AtkResNode.Width + configuration.DtrSpacing;
+
+ if (configuration.DtrSwapDirection)
+ {
+ data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
+ runningXPos += elementWidth;
+ }
+ else
+ {
+ runningXPos -= elementWidth;
+ data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2);
+ }
}
this.entries[i] = data;
}
}
+ ///
+ /// Checks if there are any Dalamud nodes in the DTR.
+ ///
+ /// True if there are nodes with an ID > 1000.
+ private bool CheckForDalamudNodes()
+ {
+ var dtr = GetDtr();
+ if (dtr == null || dtr->RootNode == null) return false;
+
+ for (var i = 0; i < dtr->UldManager.NodeListCount; i++)
+ {
+ if (dtr->UldManager.NodeList[i]->NodeID > 1000)
+ return true;
+ }
+
+ return false;
+ }
+
+ private void RecreateNodes()
+ {
+ this.runningNodeIds = BaseNodeId;
+ foreach (var entry in this.entries)
+ {
+ entry.TextNode = this.MakeNode(++this.runningNodeIds);
+ entry.Added = false;
+ }
+ }
+
private bool AddNode(AtkTextNode* node)
{
var dtr = GetDtr();
- if (dtr == null || dtr->RootNode == null || node == null) return false;
+ if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false;
var lastChild = dtr->RootNode->ChildNode;
while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode;
@@ -201,7 +256,7 @@ namespace Dalamud.Game.Gui.Dtr
private bool RemoveNode(AtkTextNode* node)
{
var dtr = GetDtr();
- if (dtr == null || dtr->RootNode == null || node == null) return false;
+ if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false;
var tmpPrevNode = node->AtkResNode.PrevSiblingNode;
var tmpNextNode = node->AtkResNode.NextSiblingNode;
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/GameFontFamily.cs b/Dalamud/Interface/GameFonts/GameFontFamily.cs
new file mode 100644
index 000000000..2aa836927
--- /dev/null
+++ b/Dalamud/Interface/GameFonts/GameFontFamily.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Dalamud.Interface.GameFonts
+{
+ ///
+ /// Enum of available game font families.
+ ///
+ public enum GameFontFamily
+ {
+ ///
+ /// Placeholder meaning unused.
+ ///
+ Undefined,
+
+ ///
+ /// Sans-serif fonts used for the whole UI. Contains Japanese characters in addition to Latin characters.
+ ///
+ Axis,
+
+ ///
+ /// Serif fonts used for job names. Contains Latin characters.
+ ///
+ Jupiter,
+
+ ///
+ /// Digit-only serif fonts used for flying texts. Contains numbers.
+ ///
+ JupiterNumeric,
+
+ ///
+ /// Digit-only sans-serif horizontally wide fonts used for HP/MP/IL numbers.
+ ///
+ Meidinger,
+
+ ///
+ /// Sans-serif horizontally wide font used for names of gauges. Contains Latin characters.
+ ///
+ MiedingerMid,
+
+ ///
+ /// Sans-serif horizontally narrow font used for addon titles. Contains Latin characters.
+ ///
+ TrumpGothic,
+ }
+}
diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs
new file mode 100644
index 000000000..1cbdf210f
--- /dev/null
+++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs
@@ -0,0 +1,174 @@
+namespace Dalamud.Interface.GameFonts
+{
+ ///
+ /// Enum of available game fonts in specific sizes.
+ ///
+ public enum GameFontFamilyAndSize : 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..a50941883
--- /dev/null
+++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Numerics;
+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 GameFontStyle fontStyle;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// GameFontManager instance.
+ /// Font to use.
+ internal GameFontHandle(GameFontManager manager, GameFontStyle font)
+ {
+ this.manager = manager;
+ this.fontStyle = font;
+ }
+
+ ///
+ /// Gets the font style.
+ ///
+ public GameFontStyle Style => this.fontStyle;
+
+ ///
+ /// Gets a value indicating whether this font is ready for use.
+ ///
+ public bool Available => this.manager.GetFont(this.fontStyle) != null;
+
+ ///
+ /// Gets the font.
+ ///
+ public ImFontPtr ImFont => this.manager.GetFont(this.fontStyle).Value;
+
+ ///
+ /// Gets the FdtReader.
+ ///
+ public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize);
+
+ ///
+ /// Creates a new GameFontLayoutPlan.Builder.
+ ///
+ /// Text.
+ /// A new builder for GameFontLayoutPlan.
+ public GameFontLayoutPlan.Builder LayoutBuilder(string text)
+ {
+ return new GameFontLayoutPlan.Builder(this.ImFont, this.FdtReader, text);
+ }
+
+ ///
+ public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle);
+
+ ///
+ /// Draws text.
+ ///
+ /// Text to draw.
+ public void Text(string text)
+ {
+ if (!this.Available)
+ {
+ ImGui.TextUnformatted(text);
+ }
+ else
+ {
+ this.LayoutBuilder(text)
+ .Build()
+ .Draw(ImGui.GetWindowDrawList(), ImGui.GetWindowPos() + ImGui.GetCursorPos(), ImGui.GetColorU32(ImGuiCol.Text));
+ }
+ }
+
+ ///
+ /// Draws text in given color.
+ ///
+ /// Color.
+ /// Text to draw.
+ public void TextColored(Vector4 col, string text)
+ {
+ ImGui.PushStyleColor(ImGuiCol.Text, col);
+ this.Text(text);
+ ImGui.PopStyleColor();
+ }
+
+ ///
+ /// Draws disabled text.
+ ///
+ /// Text to draw.
+ public void TextDisabled(string text)
+ {
+ unsafe
+ {
+ this.TextColored(*ImGui.GetStyleColorVec4(ImGuiCol.TextDisabled), text);
+ }
+ }
+ }
+}
diff --git a/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs b/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs
new file mode 100644
index 000000000..dc2d5f380
--- /dev/null
+++ b/Dalamud/Interface/GameFonts/GameFontLayoutPlan.cs
@@ -0,0 +1,414 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using System.Threading.Tasks;
+
+using ImGuiNET;
+
+namespace Dalamud.Interface.GameFonts
+{
+ ///
+ /// Plan on how glyphs will be rendered.
+ ///
+ public class GameFontLayoutPlan
+ {
+ ///
+ /// Horizontal alignment.
+ ///
+ public enum HorizontalAlignment
+ {
+ ///
+ /// Align to left.
+ ///
+ Left,
+
+ ///
+ /// Align to center.
+ ///
+ Center,
+
+ ///
+ /// Align to right.
+ ///
+ Right,
+ }
+
+ ///
+ /// Gets the associated ImFontPtr.
+ ///
+ public ImFontPtr ImFontPtr { get; internal set; }
+
+ ///
+ /// Gets the size in points of the text.
+ ///
+ public float Size { get; internal set; }
+
+ ///
+ /// Gets the x offset of the leftmost glyph.
+ ///
+ public float X { get; internal set; }
+
+ ///
+ /// Gets the width of the text.
+ ///
+ public float Width { get; internal set; }
+
+ ///
+ /// Gets the height of the text.
+ ///
+ public float Height { get; internal set; }
+
+ ///
+ /// Gets the list of plannen elements.
+ ///
+ public IList Elements { get; internal set; }
+
+ ///
+ /// Draws font to ImGui.
+ ///
+ /// Target ImDrawList.
+ /// Position.
+ /// Color.
+ public void Draw(ImDrawListPtr drawListPtr, Vector2 pos, uint col)
+ {
+ ImGui.Dummy(new Vector2(this.Width, this.Height));
+ foreach (var element in this.Elements)
+ {
+ if (element.IsControl)
+ continue;
+
+ this.ImFontPtr.RenderChar(
+ drawListPtr,
+ this.Size,
+ new Vector2(
+ this.X + pos.X + element.X,
+ pos.Y + element.Y),
+ col,
+ element.Glyph.Char);
+ }
+ }
+
+ ///
+ /// Plan on how each glyph will be rendered.
+ ///
+ public class Element
+ {
+ ///
+ /// Gets the original codepoint.
+ ///
+ public int Codepoint { get; init; }
+
+ ///
+ /// Gets the corresponding or fallback glyph.
+ ///
+ public FdtReader.FontTableEntry Glyph { get; init; }
+
+ ///
+ /// Gets the X offset of this glyph.
+ ///
+ public float X { get; internal set; }
+
+ ///
+ /// Gets the Y offset of this glyph.
+ ///
+ public float Y { get; internal set; }
+
+ ///
+ /// Gets a value indicating whether whether this codepoint is a control character.
+ ///
+ public bool IsControl
+ {
+ get
+ {
+ return this.Codepoint < 0x10000 && char.IsControl((char)this.Codepoint);
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether whether this codepoint is a space.
+ ///
+ public bool IsSpace
+ {
+ get
+ {
+ return this.Codepoint < 0x10000 && char.IsWhiteSpace((char)this.Codepoint);
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether whether this codepoint is a line break character.
+ ///
+ public bool IsLineBreak
+ {
+ get
+ {
+ return this.Codepoint == '\n' || this.Codepoint == '\r';
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether whether this codepoint is a chinese character.
+ ///
+ public bool IsChineseCharacter
+ {
+ get
+ {
+ // CJK Symbols and Punctuation(〇)
+ if (this.Codepoint >= 0x3007 && this.Codepoint <= 0x3007)
+ return true;
+
+ // CJK Unified Ideographs Extension A
+ if (this.Codepoint >= 0x3400 && this.Codepoint <= 0x4DBF)
+ return true;
+
+ // CJK Unified Ideographs
+ if (this.Codepoint >= 0x4E00 && this.Codepoint <= 0x9FFF)
+ return true;
+
+ // CJK Unified Ideographs Extension B
+ if (this.Codepoint >= 0x20000 && this.Codepoint <= 0x2A6DF)
+ return true;
+
+ // CJK Unified Ideographs Extension C
+ if (this.Codepoint >= 0x2A700 && this.Codepoint <= 0x2B73F)
+ return true;
+
+ // CJK Unified Ideographs Extension D
+ if (this.Codepoint >= 0x2B740 && this.Codepoint <= 0x2B81F)
+ return true;
+
+ // CJK Unified Ideographs Extension E
+ if (this.Codepoint >= 0x2B820 && this.Codepoint <= 0x2CEAF)
+ return true;
+
+ // CJK Unified Ideographs Extension F
+ if (this.Codepoint >= 0x2CEB0 && this.Codepoint <= 0x2EBEF)
+ return true;
+
+ return false;
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether whether this codepoint is a good position to break word after.
+ ///
+ public bool IsWordBreakPoint
+ {
+ get
+ {
+ if (this.IsChineseCharacter)
+ return true;
+
+ if (this.Codepoint >= 0x10000)
+ return false;
+
+ // TODO: Whatever
+ switch (char.GetUnicodeCategory((char)this.Codepoint))
+ {
+ case System.Globalization.UnicodeCategory.SpaceSeparator:
+ case System.Globalization.UnicodeCategory.LineSeparator:
+ case System.Globalization.UnicodeCategory.ParagraphSeparator:
+ case System.Globalization.UnicodeCategory.Control:
+ case System.Globalization.UnicodeCategory.Format:
+ case System.Globalization.UnicodeCategory.Surrogate:
+ case System.Globalization.UnicodeCategory.PrivateUse:
+ case System.Globalization.UnicodeCategory.ConnectorPunctuation:
+ case System.Globalization.UnicodeCategory.DashPunctuation:
+ case System.Globalization.UnicodeCategory.OpenPunctuation:
+ case System.Globalization.UnicodeCategory.ClosePunctuation:
+ case System.Globalization.UnicodeCategory.InitialQuotePunctuation:
+ case System.Globalization.UnicodeCategory.FinalQuotePunctuation:
+ case System.Globalization.UnicodeCategory.OtherPunctuation:
+ case System.Globalization.UnicodeCategory.MathSymbol:
+ case System.Globalization.UnicodeCategory.ModifierSymbol:
+ case System.Globalization.UnicodeCategory.OtherSymbol:
+ case System.Globalization.UnicodeCategory.OtherNotAssigned:
+ return true;
+ }
+
+ return false;
+ }
+ }
+ }
+
+ ///
+ /// Build a GameFontLayoutPlan.
+ ///
+ public class Builder
+ {
+ private readonly ImFontPtr fontPtr;
+ private readonly FdtReader fdt;
+ private readonly string text;
+ private int maxWidth = int.MaxValue;
+ private float size;
+ private HorizontalAlignment horizontalAlignment = HorizontalAlignment.Left;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Corresponding ImFontPtr.
+ /// FDT file to base on.
+ /// Text.
+ public Builder(ImFontPtr fontPtr, FdtReader fdt, string text)
+ {
+ this.fontPtr = fontPtr;
+ this.fdt = fdt;
+ this.text = text;
+ this.size = fdt.FontHeader.LineHeight;
+ }
+
+ ///
+ /// Sets the size of resulting text.
+ ///
+ /// Size in pixels.
+ /// This.
+ public Builder WithSize(float size)
+ {
+ this.size = size;
+ return this;
+ }
+
+ ///
+ /// Sets the maximum width of the text.
+ ///
+ /// Maximum width in pixels.
+ /// This.
+ public Builder WithMaxWidth(int maxWidth)
+ {
+ this.maxWidth = maxWidth;
+ return this;
+ }
+
+ ///
+ /// Sets the horizontal alignment of the text.
+ ///
+ /// Horizontal alignment.
+ /// This.
+ public Builder WithHorizontalAlignment(HorizontalAlignment horizontalAlignment)
+ {
+ this.horizontalAlignment = horizontalAlignment;
+ return this;
+ }
+
+ ///
+ /// Builds the layout plan.
+ ///
+ /// Newly created layout plan.
+ public GameFontLayoutPlan Build()
+ {
+ var scale = this.size / this.fdt.FontHeader.LineHeight;
+ var unscaledMaxWidth = (float)Math.Ceiling(this.maxWidth / scale);
+ var elements = new List();
+ foreach (var c in this.text)
+ elements.Add(new() { Codepoint = c, Glyph = this.fdt.GetGlyph(c), });
+
+ var lastBreakIndex = 0;
+ List lineBreakIndices = new() { 0 };
+ for (var i = 1; i < elements.Count; i++)
+ {
+ var prev = elements[i - 1];
+ var curr = elements[i];
+
+ if (prev.IsLineBreak)
+ {
+ curr.X = 0;
+ curr.Y = prev.Y + this.fdt.FontHeader.LineHeight;
+ lineBreakIndices.Add(i);
+ }
+ else
+ {
+ curr.X = prev.X + prev.Glyph.NextOffsetX + prev.Glyph.BoundingWidth + this.fdt.GetDistance(prev.Codepoint, curr.Codepoint);
+ curr.Y = prev.Y;
+ }
+
+ if (prev.IsWordBreakPoint)
+ lastBreakIndex = i;
+
+ if (curr.IsSpace)
+ continue;
+
+ if (curr.X + curr.Glyph.BoundingWidth < unscaledMaxWidth)
+ continue;
+
+ if (!prev.IsSpace && elements[lastBreakIndex].X > 0)
+ {
+ prev = elements[lastBreakIndex - 1];
+ curr = elements[lastBreakIndex];
+ i = lastBreakIndex;
+ }
+ else
+ {
+ lastBreakIndex = i;
+ }
+
+ curr.X = 0;
+ curr.Y = prev.Y + this.fdt.FontHeader.LineHeight;
+ lineBreakIndices.Add(i);
+ }
+
+ lineBreakIndices.Add(elements.Count);
+
+ var targetX = 0f;
+ var targetWidth = 0f;
+ var targetHeight = 0f;
+ for (var i = 1; i < lineBreakIndices.Count; i++)
+ {
+ var from = lineBreakIndices[i - 1];
+ var to = lineBreakIndices[i];
+ while (to > from && elements[to - 1].IsSpace)
+ {
+ to--;
+ }
+
+ if (from >= to)
+ continue;
+
+ var right = 0f;
+ for (var j = from; j < to; j++)
+ {
+ var e = elements[j];
+ right = Math.Max(right, e.X + Math.Max(e.Glyph.BoundingWidth, e.Glyph.AdvanceWidth));
+ targetHeight = Math.Max(targetHeight, e.Y + e.Glyph.BoundingHeight);
+ }
+
+ targetWidth = Math.Max(targetWidth, right - elements[from].X);
+ float offsetX;
+ if (this.horizontalAlignment == HorizontalAlignment.Center)
+ offsetX = (unscaledMaxWidth - right) / 2;
+ else if (this.horizontalAlignment == HorizontalAlignment.Right)
+ offsetX = unscaledMaxWidth - right;
+ else if (this.horizontalAlignment == HorizontalAlignment.Left)
+ offsetX = 0;
+ else
+ throw new ArgumentException("Invalid horizontal alignment");
+ for (var j = from; j < to; j++)
+ elements[j].X += offsetX;
+ targetX = i == 1 ? elements[from].X : Math.Min(targetX, elements[from].X);
+ }
+
+ targetHeight = Math.Max(targetHeight, this.fdt.FontHeader.LineHeight * (lineBreakIndices.Count - 1));
+
+ targetWidth *= scale;
+ targetHeight *= scale;
+ targetX *= scale;
+ foreach (var e in elements)
+ {
+ e.X *= scale;
+ e.Y *= scale;
+ }
+
+ return new GameFontLayoutPlan()
+ {
+ ImFontPtr = this.fontPtr,
+ Size = this.size,
+ X = targetX,
+ Width = targetWidth,
+ Height = targetHeight,
+ Elements = elements,
+ };
+ }
+ }
+ }
+}
diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs
new file mode 100644
index 000000000..44772bc48
--- /dev/null
+++ b/Dalamud/Interface/GameFonts/GameFontManager.cs
@@ -0,0 +1,424 @@
+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 Dictionary fonts = new();
+ private readonly Dictionary fontUseCounter = new();
+ private readonly Dictionary>> 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(GameFontFamilyAndSize font)
+ {
+ return font switch
+ {
+ GameFontFamilyAndSize.Undefined => "-",
+ GameFontFamilyAndSize.Axis96 => "AXIS (9.6pt)",
+ GameFontFamilyAndSize.Axis12 => "AXIS (12pt)",
+ GameFontFamilyAndSize.Axis14 => "AXIS (14pt)",
+ GameFontFamilyAndSize.Axis18 => "AXIS (18pt)",
+ GameFontFamilyAndSize.Axis36 => "AXIS (36pt)",
+ GameFontFamilyAndSize.Jupiter16 => "Jupiter (16pt)",
+ GameFontFamilyAndSize.Jupiter20 => "Jupiter (20pt)",
+ GameFontFamilyAndSize.Jupiter23 => "Jupiter (23pt)",
+ GameFontFamilyAndSize.Jupiter45 => "Jupiter Numeric (45pt)",
+ GameFontFamilyAndSize.Jupiter46 => "Jupiter (46pt)",
+ GameFontFamilyAndSize.Jupiter90 => "Jupiter Numeric (90pt)",
+ GameFontFamilyAndSize.Meidinger16 => "Meidinger Numeric (16pt)",
+ GameFontFamilyAndSize.Meidinger20 => "Meidinger Numeric (20pt)",
+ GameFontFamilyAndSize.Meidinger40 => "Meidinger Numeric (40pt)",
+ GameFontFamilyAndSize.MiedingerMid10 => "MiedingerMid (10pt)",
+ GameFontFamilyAndSize.MiedingerMid12 => "MiedingerMid (12pt)",
+ GameFontFamilyAndSize.MiedingerMid14 => "MiedingerMid (14pt)",
+ GameFontFamilyAndSize.MiedingerMid18 => "MiedingerMid (18pt)",
+ GameFontFamilyAndSize.MiedingerMid36 => "MiedingerMid (36pt)",
+ GameFontFamilyAndSize.TrumpGothic184 => "Trump Gothic (18.4pt)",
+ GameFontFamilyAndSize.TrumpGothic23 => "Trump Gothic (23pt)",
+ GameFontFamilyAndSize.TrumpGothic34 => "Trump Gothic (34pt)",
+ GameFontFamilyAndSize.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(GameFontFamilyAndSize font)
+ {
+ return font switch
+ {
+ GameFontFamilyAndSize.Axis96 => true,
+ GameFontFamilyAndSize.Axis12 => true,
+ GameFontFamilyAndSize.Axis14 => true,
+ GameFontFamilyAndSize.Axis18 => true,
+ GameFontFamilyAndSize.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(GameFontStyle style)
+ {
+ var needRebuild = false;
+
+ lock (this.syncRoot)
+ {
+ var prevValue = this.fontUseCounter.GetValueOrDefault(style, 0);
+ var newValue = this.fontUseCounter[style] = prevValue + 1;
+ needRebuild = (prevValue == 0) != (newValue == 0) && !this.fonts.ContainsKey(style);
+ }
+
+ if (needRebuild)
+ this.interfaceManager.RebuildFonts();
+
+ return new(this, style);
+ }
+
+ ///
+ /// Gets the font.
+ ///
+ /// Font to get.
+ /// Corresponding font or null.
+ public ImFontPtr? GetFont(GameFontStyle style) => this.fonts.GetValueOrDefault(style, null);
+
+ ///
+ /// Gets the corresponding FdtReader.
+ ///
+ /// Font to get.
+ /// Corresponding FdtReader or null.
+ public FdtReader? GetFdtReader(GameFontFamilyAndSize family) => this.fdts[(int)family];
+
+ ///
+ /// 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, GameFontStyle target, bool missingOnly, bool rebuildLookupTable)
+ {
+ GameFontManager.CopyGlyphsAcrossFonts(source, this.fonts[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(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable)
+ {
+ GameFontManager.CopyGlyphsAcrossFonts(this.fonts[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(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable)
+ {
+ GameFontManager.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable);
+ }
+
+ ///
+ /// Build fonts before plugins do something more. To be called from InterfaceManager.
+ ///
+ public void BuildFonts()
+ {
+ var io = ImGui.GetIO();
+ io.Fonts.TexDesiredWidth = 4096;
+
+ this.glyphRectIds.Clear();
+ this.fonts.Clear();
+
+ foreach (var style in this.fontUseCounter.Keys)
+ {
+ var rectIds = this.glyphRectIds[style] = new();
+
+ var fdt = this.fdts[(int)style.FamilyAndSize];
+ if (fdt == null)
+ continue;
+
+ var font = io.Fonts.AddFontDefault();
+ this.fonts[style] = font;
+ foreach (var glyph in fdt.Glyphs)
+ {
+ var c = glyph.Char;
+ if (c < 32 || c >= 0xFFFF)
+ continue;
+
+ var widthAdjustment = style.CalculateWidthAdjustment(fdt, glyph);
+ rectIds[c] = Tuple.Create(
+ io.Fonts.AddCustomRectFontGlyph(
+ font,
+ c,
+ glyph.BoundingWidth + widthAdjustment + 1,
+ glyph.BoundingHeight + 1,
+ glyph.AdvanceWidth,
+ 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;
+
+ foreach (var (style, font) in this.fonts)
+ {
+ var fdt = this.fdts[(int)style.FamilyAndSize];
+ 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[(int)style.FamilyAndSize])
+ {
+ for (var j = 0; j < 40; j++)
+ fontPtr->ConfigData->Name[j] = 0;
+ Encoding.UTF8.GetBytes(c, FontNames[(int)style.FamilyAndSize].Length, fontPtr->ConfigData->Name, 40);
+ }
+
+ foreach (var (c, (rectId, glyph)) in this.glyphRectIds[style])
+ {
+ var rc = io.Fonts.GetCustomRectByIndex(rectId);
+ var sourceBuffer = this.texturePixels[glyph.TextureFileIndex];
+ var sourceBufferDelta = glyph.TextureChannelByteIndex;
+ var widthAdjustment = style.CalculateWidthAdjustment(fdt, glyph);
+ if (widthAdjustment == 0)
+ {
+ 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;
+ }
+ }
+ }
+ else
+ {
+ for (var y = 0; y < glyph.BoundingHeight; y++)
+ {
+ for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++)
+ pixels32[((rc.Y + y) * width) + rc.X + x] = 0xFFFFFFu;
+ }
+
+ for (int xbold = 0, xbold_ = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); xbold < xbold_; xbold++)
+ {
+ var boldStrength = Math.Min(1f, style.Weight + 1 - xbold);
+ for (var y = 0; y < glyph.BoundingHeight; y++)
+ {
+ float xDelta = xbold;
+ if (style.SkewStrength > 0)
+ xDelta += style.SkewStrength * (fdt.FontHeader.LineHeight - glyph.CurrentOffsetY - y) / fdt.FontHeader.LineHeight;
+ else if (style.SkewStrength < 0)
+ xDelta -= style.SkewStrength * (glyph.CurrentOffsetY + y) / fdt.FontHeader.LineHeight;
+ var xDeltaInt = (int)Math.Floor(xDelta);
+ var xness = xDelta - xDeltaInt;
+ for (var x = 0; x < glyph.BoundingWidth; x++)
+ {
+ var sourcePixelIndex = ((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x;
+ var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)];
+ var a2 = x == glyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourceBufferDelta + (4 * (sourcePixelIndex + 1))];
+ var n = (a1 * xness) + (a2 * (1 - xness));
+ var targetOffset = ((rc.Y + y) * width) + rc.X + x + xDeltaInt;
+ pixels8[(targetOffset * 4) + 3] = Math.Max(pixels8[(targetOffset * 4) + 3], (byte)(boldStrength * n));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ foreach (var font in this.fonts.Values)
+ {
+ CopyGlyphsAcrossFonts(InterfaceManager.DefaultFont, font, true, false);
+ font.BuildLookupTable();
+ }
+ }
+
+ ///
+ /// Decrease font reference counter.
+ ///
+ /// Font to release.
+ internal void DecreaseFontRef(GameFontStyle style)
+ {
+ lock (this.syncRoot)
+ {
+ if ((this.fontUseCounter[style] -= 1) == 0)
+ this.fontUseCounter.Remove(style);
+ }
+ }
+
+ 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/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs
new file mode 100644
index 000000000..8a713f1cf
--- /dev/null
+++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs
@@ -0,0 +1,235 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Dalamud.Interface.GameFonts
+{
+ ///
+ /// Describes a font based on game resource file.
+ ///
+ public struct GameFontStyle
+ {
+ ///
+ /// Font family of the font.
+ ///
+ public GameFontFamilyAndSize FamilyAndSize;
+
+ ///
+ /// Weight of the font.
+ ///
+ /// 0 is unaltered.
+ /// Any value greater than 0 will make it bolder.
+ ///
+ public float Weight;
+
+ ///
+ /// Skewedness of the font.
+ ///
+ /// 0 is unaltered.
+ /// Greater than 1 will make upper part go rightwards.
+ /// Less than 1 will make lower part go rightwards.
+ ///
+ public float SkewStrength;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// Font family.
+ /// Size in points.
+ public GameFontStyle(GameFontFamily family, float size)
+ {
+ this.FamilyAndSize = GetRecommendedFamilyAndSize(family, size);
+ this.Weight = this.SkewStrength = 0f;
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// Font family and size.
+ public GameFontStyle(GameFontFamilyAndSize familyAndSize)
+ {
+ this.FamilyAndSize = familyAndSize;
+ this.Weight = this.SkewStrength = 0f;
+ }
+
+ ///
+ /// Gets the font family.
+ ///
+ public GameFontFamily Family => this.FamilyAndSize switch
+ {
+ GameFontFamilyAndSize.Undefined => GameFontFamily.Undefined,
+ GameFontFamilyAndSize.Axis96 => GameFontFamily.Axis,
+ GameFontFamilyAndSize.Axis12 => GameFontFamily.Axis,
+ GameFontFamilyAndSize.Axis14 => GameFontFamily.Axis,
+ GameFontFamilyAndSize.Axis18 => GameFontFamily.Axis,
+ GameFontFamilyAndSize.Axis36 => GameFontFamily.Axis,
+ GameFontFamilyAndSize.Jupiter16 => GameFontFamily.Jupiter,
+ GameFontFamilyAndSize.Jupiter20 => GameFontFamily.Jupiter,
+ GameFontFamilyAndSize.Jupiter23 => GameFontFamily.Jupiter,
+ GameFontFamilyAndSize.Jupiter45 => GameFontFamily.JupiterNumeric,
+ GameFontFamilyAndSize.Jupiter46 => GameFontFamily.Jupiter,
+ GameFontFamilyAndSize.Jupiter90 => GameFontFamily.JupiterNumeric,
+ GameFontFamilyAndSize.Meidinger16 => GameFontFamily.Meidinger,
+ GameFontFamilyAndSize.Meidinger20 => GameFontFamily.Meidinger,
+ GameFontFamilyAndSize.Meidinger40 => GameFontFamily.Meidinger,
+ GameFontFamilyAndSize.MiedingerMid10 => GameFontFamily.MiedingerMid,
+ GameFontFamilyAndSize.MiedingerMid12 => GameFontFamily.MiedingerMid,
+ GameFontFamilyAndSize.MiedingerMid14 => GameFontFamily.MiedingerMid,
+ GameFontFamilyAndSize.MiedingerMid18 => GameFontFamily.MiedingerMid,
+ GameFontFamilyAndSize.MiedingerMid36 => GameFontFamily.MiedingerMid,
+ GameFontFamilyAndSize.TrumpGothic184 => GameFontFamily.TrumpGothic,
+ GameFontFamilyAndSize.TrumpGothic23 => GameFontFamily.TrumpGothic,
+ GameFontFamilyAndSize.TrumpGothic34 => GameFontFamily.TrumpGothic,
+ GameFontFamilyAndSize.TrumpGothic68 => GameFontFamily.TrumpGothic,
+ _ => throw new InvalidOperationException(),
+ };
+
+ ///
+ /// Gets the font size.
+ ///
+ public float Size => this.FamilyAndSize switch
+ {
+ GameFontFamilyAndSize.Undefined => 0,
+ GameFontFamilyAndSize.Axis96 => 9.6f,
+ GameFontFamilyAndSize.Axis12 => 12,
+ GameFontFamilyAndSize.Axis14 => 14,
+ GameFontFamilyAndSize.Axis18 => 18,
+ GameFontFamilyAndSize.Axis36 => 36,
+ GameFontFamilyAndSize.Jupiter16 => 16,
+ GameFontFamilyAndSize.Jupiter20 => 20,
+ GameFontFamilyAndSize.Jupiter23 => 23,
+ GameFontFamilyAndSize.Jupiter45 => 45,
+ GameFontFamilyAndSize.Jupiter46 => 46,
+ GameFontFamilyAndSize.Jupiter90 => 90,
+ GameFontFamilyAndSize.Meidinger16 => 16,
+ GameFontFamilyAndSize.Meidinger20 => 20,
+ GameFontFamilyAndSize.Meidinger40 => 40,
+ GameFontFamilyAndSize.MiedingerMid10 => 10,
+ GameFontFamilyAndSize.MiedingerMid12 => 12,
+ GameFontFamilyAndSize.MiedingerMid14 => 14,
+ GameFontFamilyAndSize.MiedingerMid18 => 18,
+ GameFontFamilyAndSize.MiedingerMid36 => 36,
+ GameFontFamilyAndSize.TrumpGothic184 => 18.4f,
+ GameFontFamilyAndSize.TrumpGothic23 => 23,
+ GameFontFamilyAndSize.TrumpGothic34 => 34,
+ GameFontFamilyAndSize.TrumpGothic68 => 8,
+ _ => throw new InvalidOperationException(),
+ };
+
+ ///
+ /// Gets or sets a value indicating whether this font is bold.
+ ///
+ public bool Bold
+ {
+ get => this.Weight > 0f;
+ set => this.Weight = value ? 1f : 0f;
+ }
+
+ ///
+ /// Gets or sets a value indicating whether this font is italic.
+ ///
+ public bool Italic
+ {
+ get => this.SkewStrength != 0;
+ set => this.SkewStrength = value ? 4 : 0;
+ }
+
+ ///
+ /// Gets the recommend GameFontFamilyAndSize given family and size.
+ ///
+ /// Font family.
+ /// Font size in points.
+ /// Recommended GameFontFamilyAndSize.
+ public static GameFontFamilyAndSize GetRecommendedFamilyAndSize(GameFontFamily family, float size)
+ {
+ if (size <= 0)
+ return GameFontFamilyAndSize.Undefined;
+
+ switch (family)
+ {
+ case GameFontFamily.Undefined:
+ return GameFontFamilyAndSize.Undefined;
+
+ case GameFontFamily.Axis:
+ if (size <= 9.6)
+ return GameFontFamilyAndSize.Axis96;
+ else if (size <= 12)
+ return GameFontFamilyAndSize.Axis12;
+ else if (size <= 14)
+ return GameFontFamilyAndSize.Axis14;
+ else if (size <= 18)
+ return GameFontFamilyAndSize.Axis18;
+ else
+ return GameFontFamilyAndSize.Axis36;
+
+ case GameFontFamily.Jupiter:
+ if (size <= 16)
+ return GameFontFamilyAndSize.Jupiter16;
+ else if (size <= 20)
+ return GameFontFamilyAndSize.Jupiter20;
+ else if (size <= 23)
+ return GameFontFamilyAndSize.Jupiter23;
+ else
+ return GameFontFamilyAndSize.Jupiter46;
+
+ case GameFontFamily.JupiterNumeric:
+ if (size <= 45)
+ return GameFontFamilyAndSize.Jupiter45;
+ else
+ return GameFontFamilyAndSize.Jupiter90;
+
+ case GameFontFamily.Meidinger:
+ if (size <= 16)
+ return GameFontFamilyAndSize.Meidinger16;
+ else if (size <= 20)
+ return GameFontFamilyAndSize.Meidinger20;
+ else
+ return GameFontFamilyAndSize.Meidinger40;
+
+ case GameFontFamily.MiedingerMid:
+ if (size <= 10)
+ return GameFontFamilyAndSize.MiedingerMid10;
+ else if (size <= 12)
+ return GameFontFamilyAndSize.MiedingerMid12;
+ else if (size <= 14)
+ return GameFontFamilyAndSize.MiedingerMid14;
+ else if (size <= 18)
+ return GameFontFamilyAndSize.MiedingerMid18;
+ else
+ return GameFontFamilyAndSize.MiedingerMid36;
+
+ case GameFontFamily.TrumpGothic:
+ if (size <= 18.4)
+ return GameFontFamilyAndSize.TrumpGothic184;
+ else if (size <= 23)
+ return GameFontFamilyAndSize.TrumpGothic23;
+ else if (size <= 34)
+ return GameFontFamilyAndSize.TrumpGothic34;
+ else
+ return GameFontFamilyAndSize.TrumpGothic68;
+
+ default:
+ return GameFontFamilyAndSize.Undefined;
+ }
+ }
+
+ ///
+ /// Calculates the adjustment to width resulting fron Weight and SkewStrength.
+ ///
+ /// Font information.
+ /// Glyph.
+ /// Width adjustment in pixel unit.
+ public int CalculateWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph)
+ {
+ var widthDelta = this.Weight;
+ if (this.SkewStrength > 0)
+ widthDelta += 1f * this.SkewStrength * (reader.FontHeader.LineHeight - glyph.CurrentOffsetY) / reader.FontHeader.LineHeight;
+ else if (this.SkewStrength < 0)
+ widthDelta -= 1f * this.SkewStrength * (glyph.CurrentOffsetY + glyph.BoundingHeight) / reader.FontHeader.LineHeight;
+
+ return (int)Math.Ceiling(widthDelta);
+ }
+ }
+}
diff --git a/Dalamud/Interface/ImGuiHelpers.cs b/Dalamud/Interface/ImGuiHelpers.cs
index 4006f719e..b71b7cdd5 100644
--- a/Dalamud/Interface/ImGuiHelpers.cs
+++ b/Dalamud/Interface/ImGuiHelpers.cs
@@ -130,6 +130,12 @@ namespace Dalamud.Interface
if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{textCopy}");
}
+ ///
+ /// Write unformatted text wrapped.
+ ///
+ /// The text to write.
+ public static void SafeTextWrapped(string text) => ImGui.TextWrapped(text.Replace("%", "%%"));
+
///
/// Get data needed for each new frame.
///
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index b285c60ed..58f18bf70 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,8 @@ namespace Dalamud.Interface.Internal
private readonly SwapChainVtableResolver address;
private RawDX11Scene? scene;
+ private GameFontHandle? axisFontHandle;
+
// can't access imgui IO before first present call
private bool lastWantCapture = false;
private bool isRebuildingFonts = false;
@@ -128,10 +131,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 +312,7 @@ namespace Dalamud.Interface.Internal
if (!this.isRebuildingFonts)
{
Log.Verbose("[FONT] RebuildFonts() trigger");
+ this.SetAxisFonts();
this.isRebuildingFonts = true;
this.scene.OnNewRenderFrame += this.RebuildFontsInternal;
@@ -323,6 +332,26 @@ namespace Dalamud.Interface.Internal
Util.Fatal($"One or more files required by XIVLauncher were not found.\nPlease restart and report this error if it occurs again.\n\n{path}", "Error");
}
+ private void SetAxisFonts()
+ {
+ var configuration = Service.Get();
+ if (configuration.UseAxisFontsFromGame)
+ {
+ var currentFamilyAndSize = GameFontStyle.GetRecommendedFamilyAndSize(GameFontFamily.Axis, this.axisFontHandle?.Style.Size ?? 0f);
+ var expectedFamilyAndSize = GameFontStyle.GetRecommendedFamilyAndSize(GameFontFamily.Axis, 12 * ImGui.GetIO().FontGlobalScale);
+ if (currentFamilyAndSize == expectedFamilyAndSize)
+ return;
+
+ this.axisFontHandle?.Dispose();
+ this.axisFontHandle = Service.Get().NewFontRef(new(expectedFamilyAndSize));
+ }
+ else
+ {
+ this.axisFontHandle?.Dispose();
+ this.axisFontHandle = null;
+ }
+ }
+
/*
* NOTE(goat): When hooking ReShade DXGISwapChain::runtime_present, this is missing the syncInterval arg.
* Seems to work fine regardless, I guess, so whatever.
@@ -384,6 +413,8 @@ namespace Dalamud.Interface.Internal
this.scene.OnBuildUI += this.Display;
this.scene.OnNewInputFrame += this.OnNewInputFrame;
+ this.SetAxisFonts();
+
this.SetupFonts();
StyleModel.TransferOldModels();
@@ -496,7 +527,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 +552,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 +578,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 +592,13 @@ namespace Dalamud.Interface.Internal
ImGui.GetIO().Fonts.Build();
+ gameFontManager.AfterBuildFonts();
+ GameFontManager.CopyGlyphsAcrossFonts(this.axisFontHandle?.ImFont, 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/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
index d61ba8638..684ffc439 100644
--- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
@@ -7,6 +7,7 @@ using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+
using Dalamud.Game;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types;
@@ -80,21 +81,6 @@ namespace Dalamud.Interface.Internal.Windows
framework.Update += this.FrameworkOnUpdate;
}
- private void FrameworkOnUpdate(Framework framework)
- {
- try
- {
- if (!this.loadQueue.TryTake(out var loadAction, 0, this.downloadToken.Token))
- return;
-
- loadAction.Invoke();
- }
- catch (Exception ex)
- {
- Log.Error(ex, "An unhandled exception occurred in image loader framework dispatcher");
- }
- }
-
///
/// Gets the default plugin icon.
///
@@ -231,6 +217,21 @@ namespace Dalamud.Interface.Internal.Windows
return false;
}
+ private void FrameworkOnUpdate(Framework framework)
+ {
+ try
+ {
+ if (!this.loadQueue.TryTake(out var loadAction, 0, this.downloadToken.Token))
+ return;
+
+ loadAction.Invoke();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "An unhandled exception occurred in image loader framework dispatcher");
+ }
+ }
+
private async void DownloadTask()
{
while (!this.downloadToken.Token.IsCancellationRequested)
@@ -482,12 +483,9 @@ namespace Dalamud.Interface.Internal.Windows
var bytes = await data.Content.ReadAsByteArrayAsync();
imageBytes[i] = bytes;
-
-
Log.Verbose($"Plugin image{i + 1} for {manifest.InternalName} downloaded");
didAny = true;
-
}
if (didAny)
diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
index 81c6be519..45e5adb03 100644
--- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs
@@ -399,11 +399,11 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
if (ImGui.BeginPopupModal(modalTitle, ref this.feedbackModalDrawing, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar))
{
- ImGui.Text(Locs.FeedbackModal_Text(this.feedbackPlugin.Name));
+ ImGui.TextUnformatted(Locs.FeedbackModal_Text(this.feedbackPlugin.Name));
if (this.feedbackPlugin?.FeedbackMessage != null)
{
- ImGui.TextWrapped(this.feedbackPlugin.FeedbackMessage);
+ ImGuiHelpers.SafeTextWrapped(this.feedbackPlugin.FeedbackMessage);
}
if (this.pluginListUpdatable.Any(
@@ -1133,7 +1133,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
var cursor = ImGui.GetCursorPos();
// Name
- ImGui.Text(label);
+ ImGui.TextUnformatted(label);
// Download count
var downloadCountText = manifest.DownloadCount > 0
@@ -1164,9 +1164,9 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
if (plugin is { IsBanned: true })
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
- ImGui.TextWrapped(plugin.BanReason.IsNullOrEmpty()
- ? Locs.PluginBody_Banned
- : Locs.PluginBody_BannedReason(plugin.BanReason));
+ ImGuiHelpers.SafeTextWrapped(plugin.BanReason.IsNullOrEmpty()
+ ? Locs.PluginBody_Banned
+ : Locs.PluginBody_BannedReason(plugin.BanReason));
ImGui.PopStyleColor();
}
@@ -1176,16 +1176,16 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
{
if (!string.IsNullOrWhiteSpace(manifest.Punchline))
{
- ImGui.TextWrapped(manifest.Punchline);
+ ImGuiHelpers.SafeTextWrapped(manifest.Punchline);
}
else if (!string.IsNullOrWhiteSpace(manifest.Description))
{
const int punchlineLen = 200;
var firstLine = manifest.Description.Split(new[] { '\r', '\n' })[0];
- ImGui.TextWrapped(firstLine.Length < punchlineLen
- ? firstLine
- : firstLine[..punchlineLen]);
+ ImGuiHelpers.SafeTextWrapped(firstLine.Length < punchlineLen
+ ? firstLine
+ : firstLine[..punchlineLen]);
}
}
@@ -1225,7 +1225,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
ImGui.SameLine();
var cursor = ImGui.GetCursorPos();
- ImGui.Text(log.Title);
+ ImGui.TextUnformatted(log.Title);
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{log.Version}");
@@ -1233,7 +1233,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
cursor.Y += ImGui.GetTextLineHeightWithSpacing();
ImGui.SetCursorPos(cursor);
- ImGui.TextWrapped(log.Text);
+ ImGuiHelpers.SafeTextWrapped(log.Text);
var endCursor = ImGui.GetCursorPos();
@@ -1294,7 +1294,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
// Description
if (!string.IsNullOrWhiteSpace(manifest.Description))
{
- ImGui.TextWrapped(manifest.Description);
+ ImGuiHelpers.SafeTextWrapped(manifest.Description);
}
ImGuiHelpers.ScaledDummy(5);
@@ -1503,7 +1503,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
ImGui.Indent();
// Name
- ImGui.Text(manifest.Name);
+ ImGui.TextUnformatted(manifest.Name);
// Download count
var downloadText = plugin.IsDev
@@ -1533,7 +1533,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
// Description
if (!string.IsNullOrWhiteSpace(manifest.Description))
{
- ImGui.TextWrapped(manifest.Description);
+ ImGuiHelpers.SafeTextWrapped(manifest.Description);
}
// Available commands (if loaded)
@@ -1548,7 +1548,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
ImGui.Dummy(ImGuiHelpers.ScaledVector2(10f, 10f));
foreach (var command in commands)
{
- ImGui.TextWrapped($"{command.Key} → {command.Value.HelpMessage}");
+ ImGuiHelpers.SafeTextWrapped($"{command.Key} → {command.Value.HelpMessage}");
}
}
}
@@ -1617,7 +1617,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller
{
ImGui.Text("Changelog:");
ImGuiHelpers.ScaledDummy(2);
- ImGui.TextWrapped(manifest.Changelog);
+ ImGuiHelpers.SafeTextWrapped(manifest.Changelog);
}
ImGui.EndChild();
diff --git a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs
index 8e097955f..99ed6fef1 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;
@@ -37,6 +38,7 @@ namespace Dalamud.Interface.Internal.Windows
private bool doCfChatMessage;
private float globalUiScale;
+ private bool doUseAxisFontsFromGame;
private bool doToggleUiHide;
private bool doToggleUiHideDuringCutscenes;
private bool doToggleUiHideDuringGpose;
@@ -49,6 +51,7 @@ namespace Dalamud.Interface.Internal.Windows
private List? dtrOrder;
private List? dtrIgnore;
private int dtrSpacing;
+ private bool dtrSwapDirection;
private List thirdRepoList;
private bool thirdRepoListChanged;
@@ -88,6 +91,7 @@ namespace Dalamud.Interface.Internal.Windows
this.doCfChatMessage = configuration.DutyFinderChatMessage;
this.globalUiScale = configuration.GlobalUiScale;
+ this.doUseAxisFontsFromGame = configuration.UseAxisFontsFromGame;
this.doToggleUiHide = configuration.ToggleUiHide;
this.doToggleUiHideDuringCutscenes = configuration.ToggleUiHideDuringCutscenes;
this.doToggleUiHideDuringGpose = configuration.ToggleUiHideDuringGpose;
@@ -99,6 +103,7 @@ namespace Dalamud.Interface.Internal.Windows
this.doTsm = configuration.ShowTsm;
this.dtrSpacing = configuration.DtrSpacing;
+ this.dtrSwapDirection = configuration.DtrSwapDirection;
this.doPluginTest = configuration.DoPluginTest;
this.thirdRepoList = configuration.ThirdRepoList.Select(x => x.Clone()).ToList();
@@ -275,10 +280,14 @@ namespace Dalamud.Interface.Internal.Windows
{
this.globalUiScale = 1.0f;
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
+ Service.Get().RebuildFonts();
}
if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref this.globalUiScale, 0.005f, MinScale, MaxScale, "%.2f"))
+ {
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
+ Service.Get().RebuildFonts();
+ }
ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale all XIVLauncher UI elements - useful for 4K displays."));
@@ -295,6 +304,9 @@ namespace Dalamud.Interface.Internal.Windows
ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleUiHideOptOutNote", "Plugins may independently opt out of the settings below."));
+ ImGui.Checkbox(Loc.Localize("DalamudSettingToggleAxisFonts", "Use AXIS fonts as default Dalamud font"), ref this.doUseAxisFontsFromGame);
+ ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleUiAxisFontsHint", "Use AXIS fonts (the game's main UI fonts) as default Dalamud font."));
+
ImGui.Checkbox(Loc.Localize("DalamudSettingToggleUiHide", "Hide plugin UI when the game UI is toggled off"), ref this.doToggleUiHide);
ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingToggleUiHideHint", "Hide any open windows by plugins when toggling the game overlay."));
@@ -416,6 +428,10 @@ namespace Dalamud.Interface.Internal.Windows
ImGui.Text(Loc.Localize("DalamudSettingServerInfoBarSpacing", "Server Info Bar spacing"));
ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarSpacingHint", "Configure the amount of space between entries in the server info bar here."));
ImGui.SliderInt("Spacing", ref this.dtrSpacing, 0, 40);
+
+ ImGui.Text(Loc.Localize("DalamudSettingServerInfoBarDirection", "Server Info Bar direction"));
+ ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarDirectionHint", "If checked, the Server Info Bar elements will expand to the right instead of the left."));
+ ImGui.Checkbox("Swap Direction", ref this.dtrSwapDirection);
}
private void DrawExperimentalTab()
@@ -794,6 +810,8 @@ namespace Dalamud.Interface.Internal.Windows
configuration.IsFocusManagementEnabled = this.doFocus;
configuration.ShowTsm = this.doTsm;
+ configuration.UseAxisFontsFromGame = this.doUseAxisFontsFromGame;
+
// This is applied every frame in InterfaceManager::CheckViewportState()
configuration.IsDisableViewport = !this.doViewport;
@@ -823,6 +841,7 @@ namespace Dalamud.Interface.Internal.Windows
this.dtrIgnore = configuration.DtrIgnore;
configuration.DtrSpacing = this.dtrSpacing;
+ configuration.DtrSwapDirection = this.dtrSwapDirection;
configuration.DoPluginTest = this.doPluginTest;
configuration.ThirdRepoList = this.thirdRepoList.Select(x => x.Clone()).ToList();
@@ -836,6 +855,7 @@ namespace Dalamud.Interface.Internal.Windows
configuration.Save();
_ = Service.Get().ReloadPluginMastersAsync();
+ Service.Get().RebuildFonts();
}
}
}
diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
index c57a130b4..b4c089fbe 100644
--- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
@@ -8,6 +8,7 @@ using Dalamud.Game;
using Dalamud.Game.ClientState;
using Dalamud.Game.Gui;
using Dalamud.Interface.Animation.EasingFunctions;
+using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Windowing;
using ImGuiNET;
using ImGuiScene;
@@ -19,6 +20,7 @@ namespace Dalamud.Interface.Internal.Windows
///
internal class TitleScreenMenuWindow : Window, IDisposable
{
+ private const float TargetFontSize = 16.2f;
private readonly TextureWrap shadeTexture;
private readonly Dictionary shadeEasings = new();
@@ -27,6 +29,8 @@ namespace Dalamud.Interface.Internal.Windows
private InOutCubic? fadeOutEasing;
+ private GameFontHandle? axisFontHandle;
+
private State state = State.Hide;
///
@@ -67,14 +71,19 @@ namespace Dalamud.Interface.Internal.Windows
///
public override void PreDraw()
{
+ this.SetAxisFonts();
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0));
ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0));
+ if (this.axisFontHandle?.Available ?? false)
+ ImGui.PushFont(this.axisFontHandle.ImFont);
base.PreDraw();
}
///
public override void PostDraw()
{
+ if (this.axisFontHandle?.Available ?? false)
+ ImGui.PopFont();
ImGui.PopStyleVar(2);
base.PostDraw();
}
@@ -90,128 +99,143 @@ namespace Dalamud.Interface.Internal.Windows
///
public override void Draw()
{
- ImGui.SetWindowFontScale(1.3f);
+ ImGui.SetWindowFontScale(TargetFontSize / ImGui.GetFont().FontSize * 4 / 3);
var tsm = Service.Get();
switch (this.state)
{
case State.Show:
- {
- for (var i = 0; i < tsm.Entries.Count; i++)
{
- var entry = tsm.Entries[i];
-
- if (!this.moveEasings.TryGetValue(entry.Id, out var moveEasing))
+ for (var i = 0; i < tsm.Entries.Count; i++)
{
- moveEasing = new InOutQuint(TimeSpan.FromMilliseconds(400));
- this.moveEasings.Add(entry.Id, moveEasing);
+ var entry = tsm.Entries[i];
+
+ if (!this.moveEasings.TryGetValue(entry.Id, out var moveEasing))
+ {
+ moveEasing = new InOutQuint(TimeSpan.FromMilliseconds(400));
+ this.moveEasings.Add(entry.Id, moveEasing);
+ }
+
+ if (!moveEasing.IsRunning && !moveEasing.IsDone)
+ {
+ moveEasing.Restart();
+ }
+
+ if (moveEasing.IsDone)
+ {
+ moveEasing.Stop();
+ }
+
+ moveEasing.Update();
+
+ var finalPos = (i + 1) * this.shadeTexture.Height;
+ var pos = moveEasing.Value * finalPos;
+
+ // FIXME(goat): Sometimes, easings can overshoot and bring things out of alignment.
+ if (moveEasing.IsDone)
+ {
+ pos = finalPos;
+ }
+
+ this.DrawEntry(entry, moveEasing.IsRunning && i != 0, true, i == 0, true);
+
+ var cursor = ImGui.GetCursorPos();
+ cursor.Y = (float)pos;
+ ImGui.SetCursorPos(cursor);
}
- if (!moveEasing.IsRunning && !moveEasing.IsDone)
+ if (!ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows |
+ ImGuiHoveredFlags.AllowWhenOverlapped |
+ ImGuiHoveredFlags.AllowWhenBlockedByActiveItem))
{
- moveEasing.Restart();
+ this.state = State.FadeOut;
}
- if (moveEasing.IsDone)
- {
- moveEasing.Stop();
- }
-
- moveEasing.Update();
-
- var finalPos = (i + 1) * this.shadeTexture.Height;
- var pos = moveEasing.Value * finalPos;
-
- // FIXME(goat): Sometimes, easings can overshoot and bring things out of alignment.
- if (moveEasing.IsDone)
- {
- pos = finalPos;
- }
-
- this.DrawEntry(entry, moveEasing.IsRunning && i != 0, true, i == 0, true);
-
- var cursor = ImGui.GetCursorPos();
- cursor.Y = (float)pos;
- ImGui.SetCursorPos(cursor);
+ break;
}
- if (!ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows |
- ImGuiHoveredFlags.AllowWhenOverlapped |
- ImGuiHoveredFlags.AllowWhenBlockedByActiveItem))
- {
- this.state = State.FadeOut;
- }
-
- break;
- }
-
case State.FadeOut:
- {
- this.fadeOutEasing ??= new InOutCubic(TimeSpan.FromMilliseconds(400))
{
- IsInverse = true,
- };
+ this.fadeOutEasing ??= new InOutCubic(TimeSpan.FromMilliseconds(400))
+ {
+ IsInverse = true,
+ };
- if (!this.fadeOutEasing.IsRunning && !this.fadeOutEasing.IsDone)
- {
- this.fadeOutEasing.Restart();
+ if (!this.fadeOutEasing.IsRunning && !this.fadeOutEasing.IsDone)
+ {
+ this.fadeOutEasing.Restart();
+ }
+
+ if (this.fadeOutEasing.IsDone)
+ {
+ this.fadeOutEasing.Stop();
+ }
+
+ this.fadeOutEasing.Update();
+
+ ImGui.PushStyleVar(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value);
+
+ for (var i = 0; i < tsm.Entries.Count; i++)
+ {
+ var entry = tsm.Entries[i];
+
+ var finalPos = (i + 1) * this.shadeTexture.Height;
+
+ this.DrawEntry(entry, i != 0, true, i == 0, false);
+
+ var cursor = ImGui.GetCursorPos();
+ cursor.Y = finalPos;
+ ImGui.SetCursorPos(cursor);
+ }
+
+ ImGui.PopStyleVar();
+
+ var isHover = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows |
+ ImGuiHoveredFlags.AllowWhenOverlapped |
+ ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
+
+ if (!isHover && this.fadeOutEasing!.IsDone)
+ {
+ this.state = State.Hide;
+ this.fadeOutEasing = null;
+ }
+ else if (isHover)
+ {
+ this.state = State.Show;
+ this.fadeOutEasing = null;
+ }
+
+ break;
}
- if (this.fadeOutEasing.IsDone)
- {
- this.fadeOutEasing.Stop();
- }
-
- this.fadeOutEasing.Update();
-
- ImGui.PushStyleVar(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value);
-
- for (var i = 0; i < tsm.Entries.Count; i++)
- {
- var entry = tsm.Entries[i];
-
- var finalPos = (i + 1) * this.shadeTexture.Height;
-
- this.DrawEntry(entry, i != 0, true, i == 0, false);
-
- var cursor = ImGui.GetCursorPos();
- cursor.Y = finalPos;
- ImGui.SetCursorPos(cursor);
- }
-
- ImGui.PopStyleVar();
-
- var isHover = ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows |
- ImGuiHoveredFlags.AllowWhenOverlapped |
- ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
-
- if (!isHover && this.fadeOutEasing!.IsDone)
- {
- this.state = State.Hide;
- this.fadeOutEasing = null;
- }
- else if (isHover)
- {
- this.state = State.Show;
- this.fadeOutEasing = null;
- }
-
- break;
- }
-
case State.Hide:
- {
- if (this.DrawEntry(tsm.Entries[0], true, false, true, true))
{
- this.state = State.Show;
- }
+ if (this.DrawEntry(tsm.Entries[0], true, false, true, true))
+ {
+ this.state = State.Show;
+ }
- this.moveEasings.Clear();
- this.logoEasings.Clear();
- this.shadeEasings.Clear();
- break;
- }
+ this.moveEasings.Clear();
+ this.logoEasings.Clear();
+ this.shadeEasings.Clear();
+ break;
+ }
+ }
+ }
+
+ private void SetAxisFonts()
+ {
+ var configuration = Service.Get();
+ if (configuration.UseAxisFontsFromGame)
+ {
+ if (this.axisFontHandle == null)
+ this.axisFontHandle = Service.Get().NewFontRef(new(GameFontFamily.Axis, TargetFontSize));
+ }
+ else
+ {
+ this.axisFontHandle?.Dispose();
+ this.axisFontHandle = null;
}
}
diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs
index 445ac3618..e3f85426c 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(GameFontStyle style) => Service.Get().NewFontRef(style);
+
///
/// 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();
diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs
index ff0a7b958..e1333e072 100644
--- a/Dalamud/Interface/Windowing/Window.cs
+++ b/Dalamud/Interface/Windowing/Window.cs
@@ -121,6 +121,28 @@ namespace Dalamud.Interface.Windowing
this.IsOpen ^= true;
}
+ ///
+ /// Code to always be executed before the open-state of the window is checked.
+ ///
+ public virtual void PreOpenCheck()
+ {
+ }
+
+ ///
+ /// Additional conditions for the window to be drawn, regardless of its open-state.
+ ///
+ ///
+ /// True if the window should be drawn, false otherwise.
+ ///
+ ///
+ /// Not being drawn due to failing this condition will not change focus or trigger OnClose.
+ /// This is checked before PreDraw, but after Update.
+ ///
+ public virtual bool DrawConditions()
+ {
+ return true;
+ }
+
///
/// Code to be executed before conditionals are applied and the window is drawn.
///
@@ -170,6 +192,8 @@ namespace Dalamud.Interface.Windowing
///
internal void DrawInternal()
{
+ this.PreOpenCheck();
+
if (!this.IsOpen)
{
if (this.internalIsOpen != this.internalLastIsOpen)
@@ -184,6 +208,8 @@ namespace Dalamud.Interface.Windowing
}
this.Update();
+ if (!this.DrawConditions())
+ return;
var hasNamespace = !string.IsNullOrEmpty(this.Namespace);
diff --git a/Dalamud/Plugin/Internal/LocalPlugin.cs b/Dalamud/Plugin/Internal/LocalPlugin.cs
index f6372ecfd..ba6297103 100644
--- a/Dalamud/Plugin/Internal/LocalPlugin.cs
+++ b/Dalamud/Plugin/Internal/LocalPlugin.cs
@@ -5,6 +5,7 @@ using System.Reflection;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
+using Dalamud.Game.Gui.Dtr;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Exceptions;
@@ -416,6 +417,11 @@ namespace Dalamud.Plugin.Internal
public void Reload()
{
this.Unload(true);
+
+ // We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates.
+ var dtr = Service.Get();
+ dtr.HandleRemovedNodes();
+
this.Load(PluginLoadReason.Reload, true);
}
diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs
index dc751c448..e1ac35d11 100644
--- a/Dalamud/Plugin/Internal/PluginManager.cs
+++ b/Dalamud/Plugin/Internal/PluginManager.cs
@@ -5,7 +5,6 @@ using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
-using System.Net.Http;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
@@ -14,6 +13,7 @@ using CheapLoc;
using Dalamud.Configuration;
using Dalamud.Configuration.Internal;
using Dalamud.Game.Gui;
+using Dalamud.Game.Gui.Dtr;
using Dalamud.Game.Text;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Exceptions;
@@ -790,6 +790,10 @@ namespace Dalamud.Plugin.Internal
}
}
+ // We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates.
+ var dtr = Service.Get();
+ dtr.HandleRemovedNodes();
+
try
{
await this.InstallPluginAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update);
diff --git a/Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs b/Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs
new file mode 100644
index 000000000..04d5550a9
--- /dev/null
+++ b/Dalamud/Plugin/Ipc/Exceptions/IpcValueNullError.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace Dalamud.Plugin.Ipc.Exceptions;
+
+///
+/// This exception is thrown when a null value is passed to an IPC requiring a value type.
+///
+public class IpcValueNullError : IpcError
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Name of the IPC.
+ /// The type expected.
+ /// Index of the failing argument.
+ public IpcValueNullError(string name, Type expectedType, int index)
+ : base($"IPC {name} expects a value type({expectedType.FullName}) at index {index}, null given.")
+ {
+ // ignored
+ }
+}
diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs
index 7dbda203e..c933b4cd1 100644
--- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs
+++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs
@@ -105,7 +105,7 @@ namespace Dalamud.Plugin.Ipc.Internal
var paramTypes = methodInfo.GetParameters()
.Select(pi => pi.ParameterType).ToArray();
- if (args.Length != paramTypes.Length)
+ if (args?.Length != paramTypes.Length)
throw new IpcLengthMismatchError(this.Name, args.Length, paramTypes.Length);
for (var i = 0; i < args.Length; i++)
@@ -113,6 +113,14 @@ namespace Dalamud.Plugin.Ipc.Internal
var arg = args[i];
var paramType = paramTypes[i];
+ if (arg == null)
+ {
+ if (paramType.IsValueType)
+ throw new IpcValueNullError(this.Name, paramType, i);
+
+ continue;
+ }
+
var argType = arg.GetType();
if (argType != paramType)
{
diff --git a/build/build.csproj b/build/build.csproj
index a9b6c7ef0..270ea0a0f 100644
--- a/build/build.csproj
+++ b/build/build.csproj
@@ -7,8 +7,9 @@
IDE0002;IDE0051;IDE1006;CS0649;CS0169
..
..
+ 1
-
+