This commit is contained in:
goaaats 2022-02-27 12:52:33 +01:00
commit badce7b47a
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B
29 changed files with 2264 additions and 190 deletions

View file

@ -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

View file

@ -3,7 +3,7 @@
<AssemblyName>Dalamud.CorePlugin</AssemblyName>
<TargetFramework>net5.0-windows</TargetFramework>
<Platforms>x64</Platforms>
<LangVersion>9.0</LangVersion>
<LangVersion>10.0</LangVersion>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>

View file

@ -5,7 +5,7 @@
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64;AnyCPU</Platforms>
<LangVersion>9.0</LangVersion>
<LangVersion>10.0</LangVersion>
</PropertyGroup>
<PropertyGroup Label="Feature">

View file

@ -10,8 +10,8 @@ namespace Dalamud
/// <summary>
/// Converts a Dalamud ClientLanguage to the corresponding Lumina variant.
/// </summary>
/// <param name="language">Langauge to convert.</param>
/// <returns>Converted langauge.</returns>
/// <param name="language">Language to convert.</param>
/// <returns>Converted language.</returns>
public static Lumina.Data.Language ToLumina(this ClientLanguage language)
{
return language switch

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 a value indicating whether to use AXIS fonts from the game.
/// </summary>
public bool UseAxisFontsFromGame { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether or not plugin UI should be hidden.
/// </summary>
@ -261,6 +267,15 @@ namespace Dalamud.Configuration.Internal
/// </summary>
public int DtrSpacing { get; set; } = 10;
/// <summary>
/// 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.
/// </summary>
public bool DtrSwapDirection { get; set; } = false;
/// <summary>
/// Gets or sets a value indicating whether the title screen menu is shown.
/// </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

@ -4,11 +4,11 @@
<TargetFramework>net5.0-windows</TargetFramework>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64;AnyCPU</Platforms>
<LangVersion>9.0</LangVersion>
<LangVersion>10.0</LangVersion>
</PropertyGroup>
<PropertyGroup Label="Feature">
<DalamudVersion>6.3.0.2</DalamudVersion>
<DalamudVersion>6.3.0.4</DalamudVersion>
<Description>XIV Launcher addon framework</Description>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>

View file

@ -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 <see cref="ImGuiConfigFlags.NavEnableGamepad"/> is set.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0.0")]
public unsafe class GamepadState : IDisposable
{
private readonly Hook<ControllerPoll> gamepadPoll;
@ -32,14 +36,6 @@ namespace Dalamud.Game.ClientState.GamePad
this.gamepadPoll = new Hook<ControllerPoll>(resolver.GamepadPoll, this.GamepadPollDetour);
}
/// <summary>
/// Finalizes an instance of the <see cref="GamepadState" /> class.
/// </summary>
~GamepadState()
{
this.Dispose(false);
}
private delegate int ControllerPoll(IntPtr controllerInput);
/// <summary>
@ -164,14 +160,6 @@ namespace Dalamud.Game.ClientState.GamePad
/// <returns>1 the whole time button is pressed, 0 otherwise.</returns>
public float Raw(GamepadButtons button) => (this.ButtonsRaw & (ushort)button) > 0 ? 1 : 0;
/// <summary>
/// Enables the hook of the GamepadPoll function.
/// </summary>
public void Enable()
{
this.gamepadPoll.Enable();
}
/// <summary>
/// Disposes this instance, alongside its hooks.
/// </summary>
@ -181,6 +169,14 @@ namespace Dalamud.Game.ClientState.GamePad
GC.SuppressFinalize(this);
}
/// <summary>
/// Enables the hook of the GamepadPoll function.
/// </summary>
internal void Enable()
{
this.gamepadPoll.Enable();
}
private int GamepadPollDetour(IntPtr gamepadInput)
{
var original = this.gamepadPoll.Original(gamepadInput);

View file

@ -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<DtrBarEntry> entries = new();
private uint runningNodeIds = 1000;
private uint runningNodeIds = BaseNodeId;
/// <summary>
/// Initializes a new instance of the <see cref="DtrBar"/> 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<DalamudConfiguration>.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<Framework>.Get().Update -= this.Update;
}
/// <summary>
/// Remove nodes marked as "should be removed" from the bar.
/// </summary>
internal void HandleRemovedNodes()
{
foreach (var data in this.entries.Where(d => d.ShouldBeRemoved))
{
this.RemoveNode(data.TextNode);
}
this.entries.RemoveAll(d => d.ShouldBeRemoved);
}
/// <summary>
/// Check whether an entry with the specified title exists.
/// </summary>
@ -98,46 +117,44 @@ namespace Dalamud.Game.Gui.Dtr
var configuration = Service<DalamudConfiguration>.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<GameGui>.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<DalamudConfiguration>.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;
}
}
/// <summary>
/// Checks if there are any Dalamud nodes in the DTR.
/// </summary>
/// <returns>True if there are nodes with an ID > 1000.</returns>
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;

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,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dalamud.Interface.GameFonts
{
/// <summary>
/// Enum of available game font families.
/// </summary>
public enum GameFontFamily
{
/// <summary>
/// Placeholder meaning unused.
/// </summary>
Undefined,
/// <summary>
/// Sans-serif fonts used for the whole UI. Contains Japanese characters in addition to Latin characters.
/// </summary>
Axis,
/// <summary>
/// Serif fonts used for job names. Contains Latin characters.
/// </summary>
Jupiter,
/// <summary>
/// Digit-only serif fonts used for flying texts. Contains numbers.
/// </summary>
JupiterNumeric,
/// <summary>
/// Digit-only sans-serif horizontally wide fonts used for HP/MP/IL numbers.
/// </summary>
Meidinger,
/// <summary>
/// Sans-serif horizontally wide font used for names of gauges. Contains Latin characters.
/// </summary>
MiedingerMid,
/// <summary>
/// Sans-serif horizontally narrow font used for addon titles. Contains Latin characters.
/// </summary>
TrumpGothic,
}
}

View file

@ -0,0 +1,174 @@
namespace Dalamud.Interface.GameFonts
{
/// <summary>
/// Enum of available game fonts in specific sizes.
/// </summary>
public enum GameFontFamilyAndSize : 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,101 @@
using System;
using System.Numerics;
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 GameFontStyle fontStyle;
/// <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, GameFontStyle font)
{
this.manager = manager;
this.fontStyle = font;
}
/// <summary>
/// Gets the font style.
/// </summary>
public GameFontStyle Style => this.fontStyle;
/// <summary>
/// Gets a value indicating whether this font is ready for use.
/// </summary>
public bool Available => this.manager.GetFont(this.fontStyle) != null;
/// <summary>
/// Gets the font.
/// </summary>
public ImFontPtr ImFont => this.manager.GetFont(this.fontStyle).Value;
/// <summary>
/// Gets the FdtReader.
/// </summary>
public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize);
/// <summary>
/// Creates a new GameFontLayoutPlan.Builder.
/// </summary>
/// <param name="text">Text.</param>
/// <returns>A new builder for GameFontLayoutPlan.</returns>
public GameFontLayoutPlan.Builder LayoutBuilder(string text)
{
return new GameFontLayoutPlan.Builder(this.ImFont, this.FdtReader, text);
}
/// <inheritdoc/>
public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle);
/// <summary>
/// Draws text.
/// </summary>
/// <param name="text">Text to draw.</param>
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));
}
}
/// <summary>
/// Draws text in given color.
/// </summary>
/// <param name="col">Color.</param>
/// <param name="text">Text to draw.</param>
public void TextColored(Vector4 col, string text)
{
ImGui.PushStyleColor(ImGuiCol.Text, col);
this.Text(text);
ImGui.PopStyleColor();
}
/// <summary>
/// Draws disabled text.
/// </summary>
/// <param name="text">Text to draw.</param>
public void TextDisabled(string text)
{
unsafe
{
this.TextColored(*ImGui.GetStyleColorVec4(ImGuiCol.TextDisabled), text);
}
}
}
}

View file

@ -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
{
/// <summary>
/// Plan on how glyphs will be rendered.
/// </summary>
public class GameFontLayoutPlan
{
/// <summary>
/// Horizontal alignment.
/// </summary>
public enum HorizontalAlignment
{
/// <summary>
/// Align to left.
/// </summary>
Left,
/// <summary>
/// Align to center.
/// </summary>
Center,
/// <summary>
/// Align to right.
/// </summary>
Right,
}
/// <summary>
/// Gets the associated ImFontPtr.
/// </summary>
public ImFontPtr ImFontPtr { get; internal set; }
/// <summary>
/// Gets the size in points of the text.
/// </summary>
public float Size { get; internal set; }
/// <summary>
/// Gets the x offset of the leftmost glyph.
/// </summary>
public float X { get; internal set; }
/// <summary>
/// Gets the width of the text.
/// </summary>
public float Width { get; internal set; }
/// <summary>
/// Gets the height of the text.
/// </summary>
public float Height { get; internal set; }
/// <summary>
/// Gets the list of plannen elements.
/// </summary>
public IList<Element> Elements { get; internal set; }
/// <summary>
/// Draws font to ImGui.
/// </summary>
/// <param name="drawListPtr">Target ImDrawList.</param>
/// <param name="pos">Position.</param>
/// <param name="col">Color.</param>
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);
}
}
/// <summary>
/// Plan on how each glyph will be rendered.
/// </summary>
public class Element
{
/// <summary>
/// Gets the original codepoint.
/// </summary>
public int Codepoint { get; init; }
/// <summary>
/// Gets the corresponding or fallback glyph.
/// </summary>
public FdtReader.FontTableEntry Glyph { get; init; }
/// <summary>
/// Gets the X offset of this glyph.
/// </summary>
public float X { get; internal set; }
/// <summary>
/// Gets the Y offset of this glyph.
/// </summary>
public float Y { get; internal set; }
/// <summary>
/// Gets a value indicating whether whether this codepoint is a control character.
/// </summary>
public bool IsControl
{
get
{
return this.Codepoint < 0x10000 && char.IsControl((char)this.Codepoint);
}
}
/// <summary>
/// Gets a value indicating whether whether this codepoint is a space.
/// </summary>
public bool IsSpace
{
get
{
return this.Codepoint < 0x10000 && char.IsWhiteSpace((char)this.Codepoint);
}
}
/// <summary>
/// Gets a value indicating whether whether this codepoint is a line break character.
/// </summary>
public bool IsLineBreak
{
get
{
return this.Codepoint == '\n' || this.Codepoint == '\r';
}
}
/// <summary>
/// Gets a value indicating whether whether this codepoint is a chinese character.
/// </summary>
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;
}
}
/// <summary>
/// Gets a value indicating whether whether this codepoint is a good position to break word after.
/// </summary>
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;
}
}
}
/// <summary>
/// Build a GameFontLayoutPlan.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="Builder"/> class.
/// </summary>
/// <param name="fontPtr">Corresponding ImFontPtr.</param>
/// <param name="fdt">FDT file to base on.</param>
/// <param name="text">Text.</param>
public Builder(ImFontPtr fontPtr, FdtReader fdt, string text)
{
this.fontPtr = fontPtr;
this.fdt = fdt;
this.text = text;
this.size = fdt.FontHeader.LineHeight;
}
/// <summary>
/// Sets the size of resulting text.
/// </summary>
/// <param name="size">Size in pixels.</param>
/// <returns>This.</returns>
public Builder WithSize(float size)
{
this.size = size;
return this;
}
/// <summary>
/// Sets the maximum width of the text.
/// </summary>
/// <param name="maxWidth">Maximum width in pixels.</param>
/// <returns>This.</returns>
public Builder WithMaxWidth(int maxWidth)
{
this.maxWidth = maxWidth;
return this;
}
/// <summary>
/// Sets the horizontal alignment of the text.
/// </summary>
/// <param name="horizontalAlignment">Horizontal alignment.</param>
/// <returns>This.</returns>
public Builder WithHorizontalAlignment(HorizontalAlignment horizontalAlignment)
{
this.horizontalAlignment = horizontalAlignment;
return this;
}
/// <summary>
/// Builds the layout plan.
/// </summary>
/// <returns>Newly created layout plan.</returns>
public GameFontLayoutPlan Build()
{
var scale = this.size / this.fdt.FontHeader.LineHeight;
var unscaledMaxWidth = (float)Math.Ceiling(this.maxWidth / scale);
var elements = new List<Element>();
foreach (var c in this.text)
elements.Add(new() { Codepoint = c, Glyph = this.fdt.GetGlyph(c), });
var lastBreakIndex = 0;
List<int> 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,
};
}
}
}
}

View file

@ -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
{
/// <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 Dictionary<GameFontStyle, ImFontPtr> fonts = new();
private readonly Dictionary<GameFontStyle, int> fontUseCounter = new();
private readonly Dictionary<GameFontStyle, 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(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"),
};
}
/// <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(GameFontFamilyAndSize font)
{
return font switch
{
GameFontFamilyAndSize.Axis96 => true,
GameFontFamilyAndSize.Axis12 => true,
GameFontFamilyAndSize.Axis14 => true,
GameFontFamilyAndSize.Axis18 => true,
GameFontFamilyAndSize.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="style">Font to use.</param>
/// <returns>Handle to game font that may or may not be ready yet.</returns>
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);
}
/// <summary>
/// Gets the font.
/// </summary>
/// <param name="style">Font to get.</param>
/// <returns>Corresponding font or null.</returns>
public ImFontPtr? GetFont(GameFontStyle style) => this.fonts.GetValueOrDefault(style, null);
/// <summary>
/// Gets the corresponding FdtReader.
/// </summary>
/// <param name="family">Font to get.</param>
/// <returns>Corresponding FdtReader or null.</returns>
public FdtReader? GetFdtReader(GameFontFamilyAndSize family) => this.fdts[(int)family];
/// <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, GameFontStyle target, bool missingOnly, bool rebuildLookupTable)
{
GameFontManager.CopyGlyphsAcrossFonts(source, this.fonts[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(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable)
{
GameFontManager.CopyGlyphsAcrossFonts(this.fonts[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(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable)
{
GameFontManager.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable);
}
/// <summary>
/// Build fonts before plugins do something more. To be called from InterfaceManager.
/// </summary>
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);
}
}
}
/// <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;
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();
}
}
/// <summary>
/// Decrease font reference counter.
/// </summary>
/// <param name="style">Font to release.</param>
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);
}
}
}
}

View file

@ -0,0 +1,235 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Dalamud.Interface.GameFonts
{
/// <summary>
/// Describes a font based on game resource file.
/// </summary>
public struct GameFontStyle
{
/// <summary>
/// Font family of the font.
/// </summary>
public GameFontFamilyAndSize FamilyAndSize;
/// <summary>
/// Weight of the font.
///
/// 0 is unaltered.
/// Any value greater than 0 will make it bolder.
/// </summary>
public float Weight;
/// <summary>
/// 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.
/// </summary>
public float SkewStrength;
/// <summary>
/// Initializes a new instance of the <see cref="GameFontStyle"/> struct.
/// </summary>
/// <param name="family">Font family.</param>
/// <param name="size">Size in points.</param>
public GameFontStyle(GameFontFamily family, float size)
{
this.FamilyAndSize = GetRecommendedFamilyAndSize(family, size);
this.Weight = this.SkewStrength = 0f;
}
/// <summary>
/// Initializes a new instance of the <see cref="GameFontStyle"/> struct.
/// </summary>
/// <param name="familyAndSize">Font family and size.</param>
public GameFontStyle(GameFontFamilyAndSize familyAndSize)
{
this.FamilyAndSize = familyAndSize;
this.Weight = this.SkewStrength = 0f;
}
/// <summary>
/// Gets the font family.
/// </summary>
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(),
};
/// <summary>
/// Gets the font size.
/// </summary>
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(),
};
/// <summary>
/// Gets or sets a value indicating whether this font is bold.
/// </summary>
public bool Bold
{
get => this.Weight > 0f;
set => this.Weight = value ? 1f : 0f;
}
/// <summary>
/// Gets or sets a value indicating whether this font is italic.
/// </summary>
public bool Italic
{
get => this.SkewStrength != 0;
set => this.SkewStrength = value ? 4 : 0;
}
/// <summary>
/// Gets the recommend GameFontFamilyAndSize given family and size.
/// </summary>
/// <param name="family">Font family.</param>
/// <param name="size">Font size in points.</param>
/// <returns>Recommended GameFontFamilyAndSize.</returns>
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;
}
}
/// <summary>
/// Calculates the adjustment to width resulting fron Weight and SkewStrength.
/// </summary>
/// <param name="reader">Font information.</param>
/// <param name="glyph">Glyph.</param>
/// <returns>Width adjustment in pixel unit.</returns>
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);
}
}
}

View file

@ -130,6 +130,12 @@ namespace Dalamud.Interface
if (ImGui.IsItemClicked()) ImGui.SetClipboardText($"{textCopy}");
}
/// <summary>
/// Write unformatted text wrapped.
/// </summary>
/// <param name="text">The text to write.</param>
public static void SafeTextWrapped(string text) => ImGui.TextWrapped(text.Replace("%", "%%"));
/// <summary>
/// Get data needed for each new frame.
/// </summary>

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,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;
/// <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 +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<DalamudConfiguration>.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<GameFontManager>.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<GameFontManager>.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();

View file

@ -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");
}
}
/// <summary>
/// Gets the default plugin icon.
/// </summary>
@ -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)

View file

@ -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();

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;
@ -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<string>? dtrOrder;
private List<string>? dtrIgnore;
private int dtrSpacing;
private bool dtrSwapDirection;
private List<ThirdPartyRepoSettings> 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<InterfaceManager>.Get().RebuildFonts();
}
if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref this.globalUiScale, 0.005f, MinScale, MaxScale, "%.2f"))
{
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
Service<InterfaceManager>.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<PluginManager>.Get().ReloadPluginMastersAsync();
Service<InterfaceManager>.Get().RebuildFonts();
}
}
}

View file

@ -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
/// </summary>
internal class TitleScreenMenuWindow : Window, IDisposable
{
private const float TargetFontSize = 16.2f;
private readonly TextureWrap shadeTexture;
private readonly Dictionary<Guid, InOutCubic> shadeEasings = new();
@ -27,6 +29,8 @@ namespace Dalamud.Interface.Internal.Windows
private InOutCubic? fadeOutEasing;
private GameFontHandle? axisFontHandle;
private State state = State.Hide;
/// <summary>
@ -67,14 +71,19 @@ namespace Dalamud.Interface.Internal.Windows
/// <inheritdoc/>
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();
}
/// <inheritdoc/>
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
/// <inheritdoc/>
public override void Draw()
{
ImGui.SetWindowFontScale(1.3f);
ImGui.SetWindowFontScale(TargetFontSize / ImGui.GetFont().FontSize * 4 / 3);
var tsm = Service<TitleScreenMenu>.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<DalamudConfiguration>.Get();
if (configuration.UseAxisFontsFromGame)
{
if (this.axisFontHandle == null)
this.axisFontHandle = Service<GameFontManager>.Get().NewFontRef(new(GameFontFamily.Axis, TargetFontSize));
}
else
{
this.axisFontHandle?.Dispose();
this.axisFontHandle = null;
}
}

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="style">Font to get.</param>
/// <returns>Handle to the game font which may or may not be available for use yet.</returns>
public GameFontHandle GetGameFontHandle(GameFontStyle style) => Service<GameFontManager>.Get().NewFontRef(style);
/// <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();

View file

@ -121,6 +121,28 @@ namespace Dalamud.Interface.Windowing
this.IsOpen ^= true;
}
/// <summary>
/// Code to always be executed before the open-state of the window is checked.
/// </summary>
public virtual void PreOpenCheck()
{
}
/// <summary>
/// Additional conditions for the window to be drawn, regardless of its open-state.
/// </summary>
/// <returns>
/// True if the window should be drawn, false otherwise.
/// </returns>
/// <remarks>
/// Not being drawn due to failing this condition will not change focus or trigger OnClose.
/// This is checked before PreDraw, but after Update.
/// </remarks>
public virtual bool DrawConditions()
{
return true;
}
/// <summary>
/// Code to be executed before conditionals are applied and the window is drawn.
/// </summary>
@ -170,6 +192,8 @@ namespace Dalamud.Interface.Windowing
/// </summary>
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);

View file

@ -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<DtrBar>.Get();
dtr.HandleRemovedNodes();
this.Load(PluginLoadReason.Reload, true);
}

View file

@ -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<DtrBar>.Get();
dtr.HandleRemovedNodes();
try
{
await this.InstallPluginAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update);

View file

@ -0,0 +1,21 @@
using System;
namespace Dalamud.Plugin.Ipc.Exceptions;
/// <summary>
/// This exception is thrown when a null value is passed to an IPC requiring a value type.
/// </summary>
public class IpcValueNullError : IpcError
{
/// <summary>
/// Initializes a new instance of the <see cref="IpcValueNullError"/> class.
/// </summary>
/// <param name="name">Name of the IPC.</param>
/// <param name="expectedType">The type expected.</param>
/// <param name="index">Index of the failing argument.</param>
public IpcValueNullError(string name, Type expectedType, int index)
: base($"IPC {name} expects a value type({expectedType.FullName}) at index {index}, null given.")
{
// ignored
}
}

View file

@ -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)
{

View file

@ -7,8 +7,9 @@
<NoWarn>IDE0002;IDE0051;IDE1006;CS0649;CS0169</NoWarn>
<NukeRootDirectory>..</NukeRootDirectory>
<NukeScriptDirectory>..</NukeScriptDirectory>
<NukeTelemetryVersion>1</NukeTelemetryVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="5.3.0" />
<PackageReference Include="Nuke.Common" Version="6.0.1" />
</ItemGroup>
</Project>