mirror of
https://github.com/goatcorp/Dalamud.git
synced 2025-12-12 10:17:22 +01:00
Merge pull request #1610 from goatcorp/net8-rollup
[net8] Rollup changes from master
This commit is contained in:
commit
b483e72162
73 changed files with 9530 additions and 1736 deletions
|
|
@ -69,6 +69,10 @@ namespace Dalamud.CorePlugin
|
|||
this.Interface.UiBuilder.Draw += this.OnDraw;
|
||||
this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi;
|
||||
this.Interface.UiBuilder.OpenMainUi += this.OnOpenMainUi;
|
||||
this.Interface.UiBuilder.DefaultFontHandle.ImFontChanged += (fc, _) =>
|
||||
{
|
||||
Log.Information($"CorePlugin : DefaultFontHandle.ImFontChanged called {fc}");
|
||||
};
|
||||
|
||||
Service<CommandManager>.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = "Access the plugin." });
|
||||
|
||||
|
|
|
|||
|
|
@ -148,12 +148,9 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
|
|||
public bool UseAxisFontsFromGame { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the gamma value to apply for Dalamud fonts. Effects text thickness.
|
||||
///
|
||||
/// Before gamma is applied...
|
||||
/// * ...TTF fonts loaded with stb or FreeType are in linear space.
|
||||
/// * ...the game's prebaked AXIS fonts are in gamma space with gamma value of 1.4.
|
||||
/// Gets or sets the gamma value to apply for Dalamud fonts. Do not use.
|
||||
/// </summary>
|
||||
[Obsolete("It happens that nobody touched this setting", true)]
|
||||
public float FontGammaLevel { get; set; } = 1.4f;
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -117,6 +117,14 @@ internal sealed class Dalamud : IServiceType
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.DefaultExceptionFilter = NativeFunctions.SetUnhandledExceptionFilter(nint.Zero);
|
||||
NativeFunctions.SetUnhandledExceptionFilter(this.DefaultExceptionFilter);
|
||||
Log.Debug($"SE default exception filter at {this.DefaultExceptionFilter.ToInt64():X}");
|
||||
|
||||
var debugSig = "40 55 53 56 48 8D AC 24 ?? ?? ?? ?? B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 2B E0 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ??";
|
||||
this.DebugExceptionFilter = Service<TargetSigScanner>.Get().ScanText(debugSig);
|
||||
Log.Debug($"SE debug exception filter at {this.DebugExceptionFilter.ToInt64():X}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -128,7 +136,17 @@ internal sealed class Dalamud : IServiceType
|
|||
/// Gets location of stored assets.
|
||||
/// </summary>
|
||||
internal DirectoryInfo AssetDirectory => new(this.StartInfo.AssetDirectory!);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the in-game default exception filter.
|
||||
/// </summary>
|
||||
private nint DefaultExceptionFilter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the in-game debug exception filter.
|
||||
/// </summary>
|
||||
private nint DebugExceptionFilter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Signal to the crash handler process that we should restart the game.
|
||||
/// </summary>
|
||||
|
|
@ -191,18 +209,32 @@ internal sealed class Dalamud : IServiceType
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace the built-in exception handler with a debug one.
|
||||
/// Replace the current exception handler with the default one.
|
||||
/// </summary>
|
||||
internal void ReplaceExceptionHandler()
|
||||
{
|
||||
var releaseSig = "40 55 53 56 48 8D AC 24 ?? ?? ?? ?? B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 2B E0 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ??";
|
||||
var releaseFilter = Service<TargetSigScanner>.Get().ScanText(releaseSig);
|
||||
Log.Debug($"SE debug filter at {releaseFilter.ToInt64():X}");
|
||||
internal void UseDefaultExceptionHandler() =>
|
||||
this.SetExceptionHandler(this.DefaultExceptionFilter);
|
||||
|
||||
var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter);
|
||||
Log.Debug("Reset ExceptionFilter, old: {0}", oldFilter);
|
||||
/// <summary>
|
||||
/// Replace the current exception handler with a debug one.
|
||||
/// </summary>
|
||||
internal void UseDebugExceptionHandler() =>
|
||||
this.SetExceptionHandler(this.DebugExceptionFilter);
|
||||
|
||||
/// <summary>
|
||||
/// Disable the current exception handler.
|
||||
/// </summary>
|
||||
internal void UseNoExceptionHandler() =>
|
||||
this.SetExceptionHandler(nint.Zero);
|
||||
|
||||
/// <summary>
|
||||
/// Helper function to set the exception handler.
|
||||
/// </summary>
|
||||
private void SetExceptionHandler(nint newFilter)
|
||||
{
|
||||
var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(newFilter);
|
||||
Log.Debug("Set ExceptionFilter to {0}, old: {1}", newFilter, oldFilter);
|
||||
}
|
||||
|
||||
|
||||
private void SetupClientStructsResolver(DirectoryInfo cacheDir)
|
||||
{
|
||||
using (Timings.Start("CS Resolver Init"))
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@
|
|||
<PackageReference Include="JetBrains.Annotations" Version="2021.2.0" />
|
||||
<PackageReference Include="Lumina" Version="3.15.2" />
|
||||
<PackageReference Include="Lumina.Excel" Version="6.5.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
|
|
|||
|
|
@ -63,41 +63,48 @@ public enum DalamudAsset
|
|||
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
|
||||
[DalamudAssetPath("UIRes", "troubleIcon.png")]
|
||||
TroubleIcon = 1006,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin trouble icon overlay.
|
||||
/// </summary>
|
||||
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
|
||||
[DalamudAssetPath("UIRes", "devPluginIcon.png")]
|
||||
DevPluginIcon = 1007,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin update icon overlay.
|
||||
/// </summary>
|
||||
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
|
||||
[DalamudAssetPath("UIRes", "updateIcon.png")]
|
||||
UpdateIcon = 1007,
|
||||
UpdateIcon = 1008,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin installed icon overlay.
|
||||
/// </summary>
|
||||
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
|
||||
[DalamudAssetPath("UIRes", "installedIcon.png")]
|
||||
InstalledIcon = 1008,
|
||||
InstalledIcon = 1009,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The third party plugin icon overlay.
|
||||
/// </summary>
|
||||
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
|
||||
[DalamudAssetPath("UIRes", "thirdIcon.png")]
|
||||
ThirdIcon = 1009,
|
||||
ThirdIcon = 1010,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The installed third party plugin icon overlay.
|
||||
/// </summary>
|
||||
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
|
||||
[DalamudAssetPath("UIRes", "thirdInstalledIcon.png")]
|
||||
ThirdInstalledIcon = 1010,
|
||||
ThirdInstalledIcon = 1011,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The API bump explainer icon.
|
||||
/// </summary>
|
||||
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
|
||||
[DalamudAssetPath("UIRes", "changelogApiBump.png")]
|
||||
ChangelogApiBumpIcon = 1011,
|
||||
ChangelogApiBumpIcon = 1012,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The background shade for
|
||||
|
|
@ -105,7 +112,7 @@ public enum DalamudAsset
|
|||
/// </summary>
|
||||
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
|
||||
[DalamudAssetPath("UIRes", "tsmShade.png")]
|
||||
TitleScreenMenuShade = 1012,
|
||||
TitleScreenMenuShade = 1013,
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DalamudAssetPurpose.Font"/>: Noto Sans CJK JP Medium.
|
||||
|
|
|
|||
|
|
@ -61,6 +61,11 @@ public unsafe class Character : GameObject
|
|||
/// </summary>
|
||||
public uint MaxCp => this.Struct->CharacterData.MaxCraftingPoints;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the shield percentage of this Chara.
|
||||
/// </summary>
|
||||
public byte ShieldPercentage => this.Struct->CharacterData.ShieldValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ClassJob of this Chara.
|
||||
/// </summary>
|
||||
|
|
|
|||
159
Dalamud/Interface/GameFonts/FdtFileView.cs
Normal file
159
Dalamud/Interface/GameFonts/FdtFileView.cs
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Dalamud.Interface.GameFonts;
|
||||
|
||||
/// <summary>
|
||||
/// Reference member view of a .fdt file data.
|
||||
/// </summary>
|
||||
internal readonly unsafe struct FdtFileView
|
||||
{
|
||||
private readonly byte* ptr;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FdtFileView"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="ptr">Pointer to the data.</param>
|
||||
/// <param name="length">Length of the data.</param>
|
||||
public FdtFileView(void* ptr, int length)
|
||||
{
|
||||
this.ptr = (byte*)ptr;
|
||||
if (length < sizeof(FdtReader.FdtHeader))
|
||||
throw new InvalidDataException("Not enough space for a FdtHeader");
|
||||
|
||||
if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader))
|
||||
throw new InvalidDataException("Not enough space for a FontTableHeader");
|
||||
if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader) +
|
||||
(sizeof(FdtReader.FontTableEntry) * this.FontHeader.FontTableEntryCount))
|
||||
throw new InvalidDataException("Not enough space for all the FontTableEntry");
|
||||
|
||||
if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader))
|
||||
throw new InvalidDataException("Not enough space for a KerningTableHeader");
|
||||
if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader) +
|
||||
(sizeof(FdtReader.KerningTableEntry) * this.KerningEntryCount))
|
||||
throw new InvalidDataException("Not enough space for all the KerningTableEntry");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file header.
|
||||
/// </summary>
|
||||
public ref FdtReader.FdtHeader FileHeader => ref *(FdtReader.FdtHeader*)this.ptr;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font header.
|
||||
/// </summary>
|
||||
public ref FdtReader.FontTableHeader FontHeader =>
|
||||
ref *(FdtReader.FontTableHeader*)((nint)this.ptr + this.FileHeader.FontTableHeaderOffset);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the glyphs.
|
||||
/// </summary>
|
||||
public Span<FdtReader.FontTableEntry> Glyphs => new(this.GlyphsUnsafe, this.FontHeader.FontTableEntryCount);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the kerning header.
|
||||
/// </summary>
|
||||
public ref FdtReader.KerningTableHeader KerningHeader =>
|
||||
ref *(FdtReader.KerningTableHeader*)((nint)this.ptr + this.FileHeader.KerningTableHeaderOffset);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of kerning entries.
|
||||
/// </summary>
|
||||
public int KerningEntryCount => Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the kerning entries.
|
||||
/// </summary>
|
||||
public Span<FdtReader.KerningTableEntry> PairAdjustments => new(
|
||||
this.ptr + this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader),
|
||||
this.KerningEntryCount);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the maximum texture index.
|
||||
/// </summary>
|
||||
public int MaxTextureIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
var i = 0;
|
||||
foreach (ref var g in this.Glyphs)
|
||||
{
|
||||
if (g.TextureIndex > i)
|
||||
i = g.TextureIndex;
|
||||
}
|
||||
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
private FdtReader.FontTableEntry* GlyphsUnsafe =>
|
||||
(FdtReader.FontTableEntry*)(this.ptr + this.FileHeader.FontTableHeaderOffset +
|
||||
sizeof(FdtReader.FontTableHeader));
|
||||
|
||||
/// <summary>
|
||||
/// Finds the glyph index for the corresponding codepoint.
|
||||
/// </summary>
|
||||
/// <param name="codepoint">Unicode codepoint (UTF-32 value).</param>
|
||||
/// <returns>Corresponding index, or a negative number according to <see cref="List{T}.BinarySearch(int,int,T,System.Collections.Generic.IComparer{T}?)"/>.</returns>
|
||||
public int FindGlyphIndex(int codepoint)
|
||||
{
|
||||
var comp = FdtReader.CodePointToUtf8Int32(codepoint);
|
||||
|
||||
var glyphs = this.GlyphsUnsafe;
|
||||
var lo = 0;
|
||||
var hi = this.FontHeader.FontTableEntryCount - 1;
|
||||
while (lo <= hi)
|
||||
{
|
||||
var i = (int)(((uint)hi + (uint)lo) >> 1);
|
||||
switch (comp.CompareTo(glyphs[i].CharUtf8))
|
||||
{
|
||||
case 0:
|
||||
return i;
|
||||
case > 0:
|
||||
lo = i + 1;
|
||||
break;
|
||||
default:
|
||||
hi = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ~lo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a glyph range for use with <see cref="Interface.ManagedFontAtlas.SafeFontConfig.GlyphRanges"/>.
|
||||
/// </summary>
|
||||
/// <param name="mergeDistance">Merge two ranges into one if distance is below the value specified in this parameter.</param>
|
||||
/// <returns>Glyph ranges.</returns>
|
||||
public ushort[] ToGlyphRanges(int mergeDistance = 8)
|
||||
{
|
||||
var glyphs = this.Glyphs;
|
||||
var ranges = new List<ushort>(glyphs.Length)
|
||||
{
|
||||
checked((ushort)glyphs[0].CharInt),
|
||||
checked((ushort)glyphs[0].CharInt),
|
||||
};
|
||||
|
||||
foreach (ref var glyph in glyphs[1..])
|
||||
{
|
||||
var c32 = glyph.CharInt;
|
||||
if (c32 >= 0x10000)
|
||||
break;
|
||||
|
||||
var c16 = unchecked((ushort)c32);
|
||||
if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1])
|
||||
{
|
||||
ranges[^1] = c16;
|
||||
}
|
||||
else if (ranges[^1] + 1 < c16)
|
||||
{
|
||||
ranges.Add(c16);
|
||||
ranges.Add(c16);
|
||||
}
|
||||
}
|
||||
|
||||
ranges.Add(0);
|
||||
return ranges.ToArray();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ namespace Dalamud.Interface.GameFonts;
|
|||
/// <summary>
|
||||
/// Enum of available game fonts in specific sizes.
|
||||
/// </summary>
|
||||
public enum GameFontFamilyAndSize : int
|
||||
public enum GameFontFamilyAndSize
|
||||
{
|
||||
/// <summary>
|
||||
/// Placeholder meaning unused.
|
||||
|
|
@ -15,6 +15,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/AXIS_96.fdt", "common/font/font{0}.tex", -1)]
|
||||
Axis96,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -22,6 +23,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/AXIS_12.fdt", "common/font/font{0}.tex", -1)]
|
||||
Axis12,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -29,6 +31,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/AXIS_14.fdt", "common/font/font{0}.tex", -1)]
|
||||
Axis14,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -36,6 +39,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/AXIS_18.fdt", "common/font/font{0}.tex", -1)]
|
||||
Axis18,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -43,6 +47,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/AXIS_36.fdt", "common/font/font{0}.tex", -4)]
|
||||
Axis36,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -50,6 +55,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Serif font. Contains mostly ASCII range. Used in game for job names.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/Jupiter_16.fdt", "common/font/font{0}.tex", -1)]
|
||||
Jupiter16,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -57,6 +63,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Serif font. Contains mostly ASCII range. Used in game for job names.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/Jupiter_20.fdt", "common/font/font{0}.tex", -1)]
|
||||
Jupiter20,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -64,6 +71,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Serif font. Contains mostly ASCII range. Used in game for job names.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/Jupiter_23.fdt", "common/font/font{0}.tex", -1)]
|
||||
Jupiter23,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -71,6 +79,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Serif font. Contains mostly numbers. Used in game for flying texts.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/Jupiter_45.fdt", "common/font/font{0}.tex", -2)]
|
||||
Jupiter45,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -78,6 +87,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Serif font. Contains mostly ASCII range. Used in game for job names.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/Jupiter_46.fdt", "common/font/font{0}.tex", -2)]
|
||||
Jupiter46,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -85,6 +95,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Serif font. Contains mostly numbers. Used in game for flying texts.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/Jupiter_90.fdt", "common/font/font{0}.tex", -4)]
|
||||
Jupiter90,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -92,6 +103,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/Meidinger_16.fdt", "common/font/font{0}.tex", -1)]
|
||||
Meidinger16,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -99,6 +111,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/Meidinger_20.fdt", "common/font/font{0}.tex", -1)]
|
||||
Meidinger20,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -106,6 +119,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/Meidinger_40.fdt", "common/font/font{0}.tex", -4)]
|
||||
Meidinger40,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -113,6 +127,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Horizontally wide. Contains mostly ASCII range.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/MiedingerMid_10.fdt", "common/font/font{0}.tex", -1)]
|
||||
MiedingerMid10,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -120,6 +135,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Horizontally wide. Contains mostly ASCII range.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/MiedingerMid_12.fdt", "common/font/font{0}.tex", -1)]
|
||||
MiedingerMid12,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -127,6 +143,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Horizontally wide. Contains mostly ASCII range.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/MiedingerMid_14.fdt", "common/font/font{0}.tex", -1)]
|
||||
MiedingerMid14,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -134,6 +151,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Horizontally wide. Contains mostly ASCII range.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/MiedingerMid_18.fdt", "common/font/font{0}.tex", -1)]
|
||||
MiedingerMid18,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -141,6 +159,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Horizontally wide. Contains mostly ASCII range.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/MiedingerMid_36.fdt", "common/font/font{0}.tex", -2)]
|
||||
MiedingerMid36,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -148,6 +167,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Horizontally narrow. Contains mostly ASCII range. Used for addon titles.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/TrumpGothic_184.fdt", "common/font/font{0}.tex", -1)]
|
||||
TrumpGothic184,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -155,6 +175,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Horizontally narrow. Contains mostly ASCII range. Used for addon titles.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/TrumpGothic_23.fdt", "common/font/font{0}.tex", -1)]
|
||||
TrumpGothic23,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -162,6 +183,7 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Horizontally narrow. Contains mostly ASCII range. Used for addon titles.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/TrumpGothic_34.fdt", "common/font/font{0}.tex", -1)]
|
||||
TrumpGothic34,
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -169,5 +191,6 @@ public enum GameFontFamilyAndSize : int
|
|||
///
|
||||
/// Horizontally narrow. Contains mostly ASCII range. Used for addon titles.
|
||||
/// </summary>
|
||||
[GameFontFamilyAndSize("common/font/TrumpGothic_68.fdt", "common/font/font{0}.tex", -3)]
|
||||
TrumpGothic68,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
namespace Dalamud.Interface.GameFonts;
|
||||
|
||||
/// <summary>
|
||||
/// Marks the path for an enum value.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
internal class GameFontFamilyAndSizeAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameFontFamilyAndSizeAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="path">Inner path of the file.</param>
|
||||
/// <param name="texPathFormat">the file path format for the relevant .tex files.</param>
|
||||
/// <param name="horizontalOffset">Horizontal offset of the corresponding font.</param>
|
||||
public GameFontFamilyAndSizeAttribute(string path, string texPathFormat, int horizontalOffset)
|
||||
{
|
||||
this.Path = path;
|
||||
this.TexPathFormat = texPathFormat;
|
||||
this.HorizontalOffset = horizontalOffset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the path.
|
||||
/// </summary>
|
||||
public string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file path format for the relevant .tex files.<br />
|
||||
/// Used for <see cref="string.Format(string,object?)"/>(<see cref="TexPathFormat"/>, <see cref="int"/>).
|
||||
/// </summary>
|
||||
public string TexPathFormat { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the horizontal offset of the corresponding font.
|
||||
/// </summary>
|
||||
public int HorizontalOffset { get; }
|
||||
}
|
||||
|
|
@ -1,75 +1,102 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.GameFonts;
|
||||
|
||||
/// <summary>
|
||||
/// Prepare and keep game font loaded for use in OnDraw.
|
||||
/// ABI-compatible wrapper for <see cref="IFontHandle"/>.
|
||||
/// </summary>
|
||||
public class GameFontHandle : IDisposable
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public sealed class GameFontHandle : IFontHandle
|
||||
{
|
||||
private readonly GameFontManager manager;
|
||||
private readonly GameFontStyle fontStyle;
|
||||
private readonly GamePrebakedFontHandle fontHandle;
|
||||
private readonly FontAtlasFactory fontAtlasFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GameFontHandle"/> class.
|
||||
/// Initializes a new instance of the <see cref="GameFontHandle"/> class.<br />
|
||||
/// Ownership of <paramref name="fontHandle"/> is transferred.
|
||||
/// </summary>
|
||||
/// <param name="manager">GameFontManager instance.</param>
|
||||
/// <param name="font">Font to use.</param>
|
||||
internal GameFontHandle(GameFontManager manager, GameFontStyle font)
|
||||
/// <param name="fontHandle">The wrapped <see cref="GamePrebakedFontHandle"/>.</param>
|
||||
/// <param name="fontAtlasFactory">An instance of <see cref="FontAtlasFactory"/>.</param>
|
||||
internal GameFontHandle(GamePrebakedFontHandle fontHandle, FontAtlasFactory fontAtlasFactory)
|
||||
{
|
||||
this.manager = manager;
|
||||
this.fontStyle = font;
|
||||
this.fontHandle = fontHandle;
|
||||
this.fontAtlasFactory = fontAtlasFactory;
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// <inheritdoc />
|
||||
public event IFontHandle.ImFontChangedDelegate ImFontChanged
|
||||
{
|
||||
get
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
return this.manager.GetFont(this.fontStyle).GetValueOrDefault(null).NativePtr != null;
|
||||
}
|
||||
}
|
||||
add => this.fontHandle.ImFontChanged += value;
|
||||
remove => this.fontHandle.ImFontChanged -= value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font.
|
||||
/// </summary>
|
||||
public ImFontPtr ImFont => this.manager.GetFont(this.fontStyle).Value;
|
||||
/// <inheritdoc />
|
||||
public Exception? LoadException => this.fontHandle.LoadException;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Available => this.fontHandle.Available;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the FdtReader.
|
||||
/// Gets the font.<br />
|
||||
/// Use of this properly is safe only from the UI thread.<br />
|
||||
/// Use <see cref="IFontHandle.Push"/> if the intended purpose of this property is <see cref="ImGui.PushFont"/>.<br />
|
||||
/// Futures changes may make simple <see cref="ImGui.PushFont"/> not enough.<br />
|
||||
/// If you need to access a font outside the UI thread, use <see cref="IFontHandle.Lock"/>.
|
||||
/// </summary>
|
||||
public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize);
|
||||
[Obsolete($"Use {nameof(Push)}-{nameof(ImGui.GetFont)} or {nameof(Lock)} instead.", false)]
|
||||
public ImFontPtr ImFont => this.fontHandle.LockUntilPostFrame();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new GameFontLayoutPlan.Builder.
|
||||
/// Gets the font style. Only applicable for <see cref="GameFontHandle"/>.
|
||||
/// </summary>
|
||||
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
|
||||
public GameFontStyle Style => ((GamePrebakedFontHandle)this.fontHandle).FontStyle;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the relevant <see cref="FdtReader"/>.<br />
|
||||
/// <br />
|
||||
/// Only applicable for game fonts. Otherwise it will throw.
|
||||
/// </summary>
|
||||
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
|
||||
public FdtReader FdtReader => this.fontAtlasFactory.GetFdtReader(this.Style.FamilyAndSize)!;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => this.fontHandle.Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public ILockedImFont Lock() => this.fontHandle.Lock();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDisposable Push() => this.fontHandle.Push();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Pop() => this.fontHandle.Pop();
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IFontHandle> WaitAsync() => this.fontHandle.WaitAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="GameFontLayoutPlan.Builder"/>.<br />
|
||||
/// <br />
|
||||
/// Only applicable for game fonts. Otherwise it will throw.
|
||||
/// </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);
|
||||
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
|
||||
public GameFontLayoutPlan.Builder LayoutBuilder(string text) => new(this.ImFont, this.FdtReader, text);
|
||||
|
||||
/// <summary>
|
||||
/// Draws text.
|
||||
/// </summary>
|
||||
/// <param name="text">Text to draw.</param>
|
||||
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
|
||||
public void Text(string text)
|
||||
{
|
||||
if (!this.Available)
|
||||
|
|
@ -93,6 +120,7 @@ public class GameFontHandle : IDisposable
|
|||
/// </summary>
|
||||
/// <param name="col">Color.</param>
|
||||
/// <param name="text">Text to draw.</param>
|
||||
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
|
||||
public void TextColored(Vector4 col, string text)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, col);
|
||||
|
|
@ -104,6 +132,7 @@ public class GameFontHandle : IDisposable
|
|||
/// Draws disabled text.
|
||||
/// </summary>
|
||||
/// <param name="text">Text to draw.</param>
|
||||
[Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)]
|
||||
public void TextDisabled(string text)
|
||||
{
|
||||
unsafe
|
||||
|
|
|
|||
|
|
@ -1,507 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility.Timing;
|
||||
using ImGuiNET;
|
||||
using Lumina.Data.Files;
|
||||
using Serilog;
|
||||
|
||||
using static Dalamud.Interface.Utility.ImGuiHelpers;
|
||||
|
||||
namespace Dalamud.Interface.GameFonts;
|
||||
|
||||
/// <summary>
|
||||
/// Loads game font for use in ImGui.
|
||||
/// </summary>
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal class GameFontManager : IServiceType
|
||||
{
|
||||
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 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();
|
||||
|
||||
#pragma warning disable CS0414
|
||||
private bool isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false;
|
||||
#pragma warning restore CS0414
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private GameFontManager(DataManager dataManager)
|
||||
{
|
||||
using (Timings.Start("Getting fdt data"))
|
||||
{
|
||||
this.fdts = FontNames.Select(fontName => fontName == null ? null : new FdtReader(dataManager.GetFile($"common/font/{fontName}.fdt")!.Data)).ToArray();
|
||||
}
|
||||
|
||||
using (Timings.Start("Getting texture data"))
|
||||
{
|
||||
var texTasks = Enumerable
|
||||
.Range(1, 1 + this.fdts
|
||||
.Where(x => x != null)
|
||||
.Select(x => x.Glyphs.Select(y => y.TextureFileIndex).Max())
|
||||
.Max())
|
||||
.Select(x => dataManager.GetFile<TexFile>($"common/font/font{x}.tex")!)
|
||||
.Select(x => new Task<byte[]>(Timings.AttachTimingHandle(() => x.ImageData!)))
|
||||
.ToArray();
|
||||
foreach (var task in texTasks)
|
||||
task.Start();
|
||||
this.texturePixels = texTasks.Select(x => x.GetAwaiter().GetResult()).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Unscales fonts after they have been rendered onto atlas.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">Font to unscale.</param>
|
||||
/// <param name="fontScale">Scale factor.</param>
|
||||
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
|
||||
public static void UnscaleFont(ImFontPtr fontPtr, float fontScale, bool rebuildLookupTable = true)
|
||||
{
|
||||
if (fontScale == 1)
|
||||
return;
|
||||
|
||||
unsafe
|
||||
{
|
||||
var font = fontPtr.NativePtr;
|
||||
for (int i = 0, i_ = font->IndexedHotData.Size; i < i_; ++i)
|
||||
{
|
||||
font->IndexedHotData.Ref<ImFontGlyphHotDataReal>(i).AdvanceX /= fontScale;
|
||||
font->IndexedHotData.Ref<ImFontGlyphHotDataReal>(i).OccupiedWidth /= fontScale;
|
||||
}
|
||||
|
||||
font->FontSize /= fontScale;
|
||||
font->Ascent /= fontScale;
|
||||
font->Descent /= fontScale;
|
||||
if (font->ConfigData != null)
|
||||
font->ConfigData->SizePixels /= fontScale;
|
||||
var glyphs = (ImFontGlyphReal*)font->Glyphs.Data;
|
||||
for (int i = 0, i_ = font->Glyphs.Size; i < i_; i++)
|
||||
{
|
||||
var glyph = &glyphs[i];
|
||||
glyph->X0 /= fontScale;
|
||||
glyph->X1 /= fontScale;
|
||||
glyph->Y0 /= fontScale;
|
||||
glyph->Y1 /= fontScale;
|
||||
glyph->AdvanceX /= fontScale;
|
||||
}
|
||||
|
||||
for (int i = 0, i_ = font->KerningPairs.Size; i < i_; i++)
|
||||
font->KerningPairs.Ref<ImFontKerningPair>(i).AdvanceXAdjustment /= fontScale;
|
||||
for (int i = 0, i_ = font->FrequentKerningPairs.Size; i < i_; i++)
|
||||
font->FrequentKerningPairs.Ref<float>(i) /= fontScale;
|
||||
}
|
||||
|
||||
if (rebuildLookupTable && fontPtr.Glyphs.Size > 0)
|
||||
fontPtr.BuildLookupTableNonstandard();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a glyph range for use with ImGui AddFont.
|
||||
/// </summary>
|
||||
/// <param name="family">Font family and size.</param>
|
||||
/// <param name="mergeDistance">Merge two ranges into one if distance is below the value specified in this parameter.</param>
|
||||
/// <returns>Glyph ranges.</returns>
|
||||
public GCHandle ToGlyphRanges(GameFontFamilyAndSize family, int mergeDistance = 8)
|
||||
{
|
||||
var fdt = this.fdts[(int)family]!;
|
||||
var ranges = new List<ushort>(fdt.Glyphs.Count)
|
||||
{
|
||||
checked((ushort)fdt.Glyphs[0].CharInt),
|
||||
checked((ushort)fdt.Glyphs[0].CharInt),
|
||||
};
|
||||
|
||||
foreach (var glyph in fdt.Glyphs.Skip(1))
|
||||
{
|
||||
var c32 = glyph.CharInt;
|
||||
if (c32 >= 0x10000)
|
||||
break;
|
||||
|
||||
var c16 = unchecked((ushort)c32);
|
||||
if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1])
|
||||
{
|
||||
ranges[^1] = c16;
|
||||
}
|
||||
else if (ranges[^1] + 1 < c16)
|
||||
{
|
||||
ranges.Add(c16);
|
||||
ranges.Add(c16);
|
||||
}
|
||||
}
|
||||
|
||||
return GCHandle.Alloc(ranges.ToArray(), GCHandleType.Pinned);
|
||||
}
|
||||
|
||||
/// <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 interfaceManager = Service<InterfaceManager>.Get();
|
||||
var needRebuild = false;
|
||||
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
this.fontUseCounter[style] = this.fontUseCounter.GetValueOrDefault(style, 0) + 1;
|
||||
}
|
||||
|
||||
needRebuild = !this.fonts.ContainsKey(style);
|
||||
if (needRebuild)
|
||||
{
|
||||
Log.Information("[GameFontManager] NewFontRef: Queueing RebuildFonts because {0} has been requested.", style.ToString());
|
||||
Service<Framework>.GetAsync()
|
||||
.ContinueWith(task => task.Result.RunOnTick(() => 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)
|
||||
{
|
||||
ImGuiHelpers.CopyGlyphsAcrossFonts(source ?? default, 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)
|
||||
{
|
||||
ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target ?? default, 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)
|
||||
{
|
||||
ImGuiHelpers.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()
|
||||
{
|
||||
this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = true;
|
||||
|
||||
this.glyphRectIds.Clear();
|
||||
this.fonts.Clear();
|
||||
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
foreach (var style in this.fontUseCounter.Keys)
|
||||
this.EnsureFont(style);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Record that ImGui.GetIO().Fonts.Build() has been called.
|
||||
/// </summary>
|
||||
public void AfterIoFontsBuild()
|
||||
{
|
||||
this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether GameFontMamager owns an ImFont.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">ImFontPtr to check.</param>
|
||||
/// <returns>Whether it owns.</returns>
|
||||
public bool OwnsFont(ImFontPtr fontPtr) => this.fonts.ContainsValue(fontPtr);
|
||||
|
||||
/// <summary>
|
||||
/// Post-build fonts before plugins do something more. To be called from InterfaceManager.
|
||||
/// </summary>
|
||||
public unsafe void AfterBuildFonts()
|
||||
{
|
||||
var interfaceManager = Service<InterfaceManager>.Get();
|
||||
var ioFonts = ImGui.GetIO().Fonts;
|
||||
var fontGamma = interfaceManager.FontGamma;
|
||||
|
||||
var pixels8s = new byte*[ioFonts.Textures.Size];
|
||||
var pixels32s = new uint*[ioFonts.Textures.Size];
|
||||
var widths = new int[ioFonts.Textures.Size];
|
||||
var heights = new int[ioFonts.Textures.Size];
|
||||
for (var i = 0; i < pixels8s.Length; i++)
|
||||
{
|
||||
ioFonts.GetTexDataAsRGBA32(i, out pixels8s[i], out widths[i], out heights[i]);
|
||||
pixels32s[i] = (uint*)pixels8s[i];
|
||||
}
|
||||
|
||||
foreach (var (style, font) in this.fonts)
|
||||
{
|
||||
var fdt = this.fdts[(int)style.FamilyAndSize];
|
||||
var scale = style.SizePt / fdt.FontHeader.Size;
|
||||
var fontPtr = font.NativePtr;
|
||||
|
||||
Log.Verbose("[GameFontManager] AfterBuildFonts: Scaling {0} from {1}pt to {2}pt (scale: {3})", style.ToString(), fdt.FontHeader.Size, style.SizePt, scale);
|
||||
|
||||
fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3;
|
||||
if (fontPtr->ConfigData != null)
|
||||
fontPtr->ConfigData->SizePixels = fontPtr->FontSize;
|
||||
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)
|
||||
{
|
||||
var ptr = font.NativePtr;
|
||||
ptr->FallbackChar = fallbackCharCandidate;
|
||||
ptr->FallbackGlyph = glyph.NativePtr;
|
||||
ptr->FallbackHotData = (ImFontGlyphHotData*)ptr->IndexedHotData.Address<ImFontGlyphHotDataReal>(fallbackCharCandidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// I have no idea what's causing NPE, so just to be safe
|
||||
try
|
||||
{
|
||||
if (font.NativePtr != null && font.NativePtr->ConfigData != null)
|
||||
{
|
||||
var nameBytes = Encoding.UTF8.GetBytes(style.ToString() + "\0");
|
||||
Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count));
|
||||
}
|
||||
}
|
||||
catch (NullReferenceException)
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
|
||||
foreach (var (c, (rectId, glyph)) in this.glyphRectIds[style])
|
||||
{
|
||||
var rc = (ImFontAtlasCustomRectReal*)ioFonts.GetCustomRectByIndex(rectId).NativePtr;
|
||||
var pixels8 = pixels8s[rc->TextureIndex];
|
||||
var pixels32 = pixels32s[rc->TextureIndex];
|
||||
var width = widths[rc->TextureIndex];
|
||||
var height = heights[rc->TextureIndex];
|
||||
var sourceBuffer = this.texturePixels[glyph.TextureFileIndex];
|
||||
var sourceBufferDelta = glyph.TextureChannelByteIndex;
|
||||
var widthAdjustment = style.CalculateBaseWidthAdjustment(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.BaseSkewStrength > 0)
|
||||
xDelta += style.BaseSkewStrength * (fdt.FontHeader.LineHeight - glyph.CurrentOffsetY - y) / fdt.FontHeader.LineHeight;
|
||||
else if (style.BaseSkewStrength < 0)
|
||||
xDelta -= style.BaseSkewStrength * (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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.Abs(fontGamma - 1.4f) >= 0.001)
|
||||
{
|
||||
// Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4)
|
||||
for (int y = rc->Y, y_ = rc->Y + rc->Height; y < y_; y++)
|
||||
{
|
||||
for (int x = rc->X, x_ = rc->X + rc->Width; x < x_; x++)
|
||||
{
|
||||
var i = (((y * width) + x) * 4) + 3;
|
||||
pixels8[i] = (byte)(Math.Pow(pixels8[i] / 255.0f, 1.4f / fontGamma) * 255.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UnscaleFont(font, 1 / scale, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrease font reference counter.
|
||||
/// </summary>
|
||||
/// <param name="style">Font to release.</param>
|
||||
internal void DecreaseFontRef(GameFontStyle style)
|
||||
{
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
if (!this.fontUseCounter.ContainsKey(style))
|
||||
return;
|
||||
|
||||
if ((this.fontUseCounter[style] -= 1) == 0)
|
||||
this.fontUseCounter.Remove(style);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void EnsureFont(GameFontStyle style)
|
||||
{
|
||||
var rectIds = this.glyphRectIds[style] = new();
|
||||
|
||||
var fdt = this.fdts[(int)style.FamilyAndSize];
|
||||
if (fdt == null)
|
||||
return;
|
||||
|
||||
ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig();
|
||||
fontConfig.OversampleH = 1;
|
||||
fontConfig.OversampleV = 1;
|
||||
fontConfig.PixelSnapH = false;
|
||||
|
||||
var io = ImGui.GetIO();
|
||||
var font = io.Fonts.AddFontDefault(fontConfig);
|
||||
|
||||
fontConfig.Destroy();
|
||||
|
||||
this.fonts[style] = font;
|
||||
foreach (var glyph in fdt.Glyphs)
|
||||
{
|
||||
var c = glyph.Char;
|
||||
if (c < 32 || c >= 0xFFFF)
|
||||
continue;
|
||||
|
||||
var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph);
|
||||
rectIds[c] = Tuple.Create(
|
||||
io.Fonts.AddCustomRectFontGlyph(
|
||||
font,
|
||||
c,
|
||||
glyph.BoundingWidth + widthAdjustment,
|
||||
glyph.BoundingHeight,
|
||||
glyph.AdvanceWidth,
|
||||
new Vector2(0, glyph.CurrentOffsetY)),
|
||||
glyph);
|
||||
}
|
||||
|
||||
foreach (var kernPair in fdt.Distances)
|
||||
font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset);
|
||||
}
|
||||
}
|
||||
|
|
@ -64,7 +64,7 @@ public struct GameFontStyle
|
|||
/// </summary>
|
||||
public float SizePt
|
||||
{
|
||||
get => this.SizePx * 3 / 4;
|
||||
readonly get => this.SizePx * 3 / 4;
|
||||
set => this.SizePx = value * 4 / 3;
|
||||
}
|
||||
|
||||
|
|
@ -73,14 +73,14 @@ public struct GameFontStyle
|
|||
/// </summary>
|
||||
public float BaseSkewStrength
|
||||
{
|
||||
get => this.SkewStrength * this.BaseSizePx / this.SizePx;
|
||||
readonly get => this.SkewStrength * this.BaseSizePx / this.SizePx;
|
||||
set => this.SkewStrength = value * this.SizePx / this.BaseSizePx;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font family.
|
||||
/// </summary>
|
||||
public GameFontFamily Family => this.FamilyAndSize switch
|
||||
public readonly GameFontFamily Family => this.FamilyAndSize switch
|
||||
{
|
||||
GameFontFamilyAndSize.Undefined => GameFontFamily.Undefined,
|
||||
GameFontFamilyAndSize.Axis96 => GameFontFamily.Axis,
|
||||
|
|
@ -112,7 +112,7 @@ public struct GameFontStyle
|
|||
/// <summary>
|
||||
/// Gets the corresponding GameFontFamilyAndSize but with minimum possible font sizes.
|
||||
/// </summary>
|
||||
public GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch
|
||||
public readonly GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch
|
||||
{
|
||||
GameFontFamily.Axis => GameFontFamilyAndSize.Axis96,
|
||||
GameFontFamily.Jupiter => GameFontFamilyAndSize.Jupiter16,
|
||||
|
|
@ -126,7 +126,7 @@ public struct GameFontStyle
|
|||
/// <summary>
|
||||
/// Gets the base font size in point unit.
|
||||
/// </summary>
|
||||
public float BaseSizePt => this.FamilyAndSize switch
|
||||
public readonly float BaseSizePt => this.FamilyAndSize switch
|
||||
{
|
||||
GameFontFamilyAndSize.Undefined => 0,
|
||||
GameFontFamilyAndSize.Axis96 => 9.6f,
|
||||
|
|
@ -158,14 +158,14 @@ public struct GameFontStyle
|
|||
/// <summary>
|
||||
/// Gets the base font size in pixel unit.
|
||||
/// </summary>
|
||||
public float BaseSizePx => this.BaseSizePt * 4 / 3;
|
||||
public readonly float BaseSizePx => this.BaseSizePt * 4 / 3;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this font is bold.
|
||||
/// </summary>
|
||||
public bool Bold
|
||||
{
|
||||
get => this.Weight > 0f;
|
||||
readonly get => this.Weight > 0f;
|
||||
set => this.Weight = value ? 1f : 0f;
|
||||
}
|
||||
|
||||
|
|
@ -174,8 +174,8 @@ public struct GameFontStyle
|
|||
/// </summary>
|
||||
public bool Italic
|
||||
{
|
||||
get => this.SkewStrength != 0;
|
||||
set => this.SkewStrength = value ? this.SizePx / 7 : 0;
|
||||
readonly get => this.SkewStrength != 0;
|
||||
set => this.SkewStrength = value ? this.SizePx / 6 : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -233,13 +233,26 @@ public struct GameFontStyle
|
|||
_ => GameFontFamilyAndSize.Undefined,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new scaled instance of <see cref="GameFontStyle"/> struct.
|
||||
/// </summary>
|
||||
/// <param name="scale">The scale.</param>
|
||||
/// <returns>The scaled instance.</returns>
|
||||
public readonly GameFontStyle Scale(float scale) => new()
|
||||
{
|
||||
FamilyAndSize = GetRecommendedFamilyAndSize(this.Family, this.SizePt * scale),
|
||||
SizePx = this.SizePx * scale,
|
||||
Weight = this.Weight,
|
||||
SkewStrength = this.SkewStrength * scale,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the adjustment to width resulting fron Weight and SkewStrength.
|
||||
/// </summary>
|
||||
/// <param name="header">Font header.</param>
|
||||
/// <param name="glyph">Glyph.</param>
|
||||
/// <returns>Width adjustment in pixel unit.</returns>
|
||||
public int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph)
|
||||
public readonly int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph)
|
||||
{
|
||||
var widthDelta = this.Weight;
|
||||
switch (this.BaseSkewStrength)
|
||||
|
|
@ -263,11 +276,11 @@ public struct GameFontStyle
|
|||
/// <param name="reader">Font information.</param>
|
||||
/// <param name="glyph">Glyph.</param>
|
||||
/// <returns>Width adjustment in pixel unit.</returns>
|
||||
public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) =>
|
||||
public readonly int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) =>
|
||||
this.CalculateBaseWidthAdjustment(reader.FontHeader, glyph);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
public override readonly string ToString()
|
||||
{
|
||||
return $"GameFontStyle({this.FamilyAndSize}, {this.SizePt}pt, skew={this.SkewStrength}, weight={this.Weight})";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ using System.Text.Unicode;
|
|||
using Dalamud.Game.Text;
|
||||
using Dalamud.Hooking.WndProcHook;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
|
@ -196,9 +197,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType
|
|||
{
|
||||
if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length))
|
||||
{
|
||||
if (Service<GameFontManager>.Get()
|
||||
.GetFdtReader(GameFontFamilyAndSize.Axis12)
|
||||
?.FindGlyph(chr) is null)
|
||||
if (Service<FontAtlasFactory>.Get()
|
||||
?.GetFdtReader(GameFontFamilyAndSize.Axis12)
|
||||
.FindGlyph(chr) is null)
|
||||
{
|
||||
if (!this.EncounteredHan)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
|||
using Dalamud.Interface.Internal.Windows.SelfTest;
|
||||
using Dalamud.Interface.Internal.Windows.Settings;
|
||||
using Dalamud.Interface.Internal.Windows.StyleEditor;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Style;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
|
@ -93,7 +94,8 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
private DalamudInterface(
|
||||
Dalamud dalamud,
|
||||
DalamudConfiguration configuration,
|
||||
InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene,
|
||||
FontAtlasFactory fontAtlasFactory,
|
||||
InterfaceManager interfaceManager,
|
||||
PluginImageCache pluginImageCache,
|
||||
DalamudAssetManager dalamudAssetManager,
|
||||
Game.Framework framework,
|
||||
|
|
@ -103,7 +105,7 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
{
|
||||
this.dalamud = dalamud;
|
||||
this.configuration = configuration;
|
||||
this.interfaceManager = interfaceManagerWithScene.Manager;
|
||||
this.interfaceManager = interfaceManager;
|
||||
|
||||
this.WindowSystem = new WindowSystem("DalamudCore");
|
||||
|
||||
|
|
@ -122,10 +124,14 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
clientState,
|
||||
configuration,
|
||||
dalamudAssetManager,
|
||||
fontAtlasFactory,
|
||||
framework,
|
||||
gameGui,
|
||||
titleScreenMenu) { IsOpen = false };
|
||||
this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false };
|
||||
this.changelogWindow = new ChangelogWindow(
|
||||
this.titleScreenMenuWindow,
|
||||
fontAtlasFactory,
|
||||
dalamudAssetManager) { IsOpen = false };
|
||||
this.profilerWindow = new ProfilerWindow() { IsOpen = false };
|
||||
this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false };
|
||||
this.hitchSettingsWindow = new HitchSettingsWindow() { IsOpen = false };
|
||||
|
|
@ -207,6 +213,7 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
{
|
||||
this.interfaceManager.Draw -= this.OnDraw;
|
||||
|
||||
this.WindowSystem.Windows.OfType<IDisposable>().AggregateToDisposable().Dispose();
|
||||
this.WindowSystem.RemoveAllWindows();
|
||||
|
||||
this.changelogWindow.Dispose();
|
||||
|
|
@ -660,7 +667,7 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
}
|
||||
|
||||
var antiDebug = Service<AntiDebug>.Get();
|
||||
if (ImGui.MenuItem("Enable AntiDebug", null, antiDebug.IsEnabled))
|
||||
if (ImGui.MenuItem("Disable Debugging Protections", null, antiDebug.IsEnabled))
|
||||
{
|
||||
var newEnabled = !antiDebug.IsEnabled;
|
||||
if (newEnabled)
|
||||
|
|
@ -856,9 +863,19 @@ internal class DalamudInterface : IDisposable, IServiceType
|
|||
|
||||
if (ImGui.BeginMenu("Game"))
|
||||
{
|
||||
if (ImGui.MenuItem("Replace ExceptionHandler"))
|
||||
if (ImGui.MenuItem("Use in-game default ExceptionHandler"))
|
||||
{
|
||||
this.dalamud.ReplaceExceptionHandler();
|
||||
this.dalamud.UseDefaultExceptionHandler();
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Use in-game debug ExceptionHandler"))
|
||||
{
|
||||
this.dalamud.UseDebugExceptionHandler();
|
||||
}
|
||||
|
||||
if (ImGui.MenuItem("Disable in-game ExceptionHandler"))
|
||||
{
|
||||
this.dalamud.UseNoExceptionHandler();
|
||||
}
|
||||
|
||||
ImGui.EndMenu();
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,3 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
|
|
@ -7,6 +6,8 @@ using Dalamud.Interface.Animation.EasingFunctions;
|
|||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
|
|
@ -31,8 +32,14 @@ internal sealed class ChangelogWindow : Window, IDisposable
|
|||
• Plugins can now add tooltips and interaction to the server info bar
|
||||
• The Dalamud/plugin installer UI has been refreshed
|
||||
";
|
||||
|
||||
|
||||
private readonly TitleScreenMenuWindow tsmWindow;
|
||||
|
||||
private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new();
|
||||
private readonly IFontAtlas privateAtlas;
|
||||
private readonly Lazy<IFontHandle> bannerFont;
|
||||
private readonly Lazy<IDalamudTextureWrap> apiBumpExplainerTexture;
|
||||
private readonly Lazy<IDalamudTextureWrap> logoTexture;
|
||||
|
||||
private readonly InOutCubic windowFade = new(TimeSpan.FromSeconds(2.5f))
|
||||
{
|
||||
|
|
@ -46,27 +53,36 @@ internal sealed class ChangelogWindow : Window, IDisposable
|
|||
Point2 = Vector2.One,
|
||||
};
|
||||
|
||||
private IDalamudTextureWrap? apiBumpExplainerTexture;
|
||||
private IDalamudTextureWrap? logoTexture;
|
||||
private GameFontHandle? bannerFont;
|
||||
|
||||
private State state = State.WindowFadeIn;
|
||||
|
||||
private bool needFadeRestart = false;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ChangelogWindow"/> class.
|
||||
/// </summary>
|
||||
/// <param name="tsmWindow">TSM window.</param>
|
||||
public ChangelogWindow(TitleScreenMenuWindow tsmWindow)
|
||||
/// <param name="fontAtlasFactory">An instance of <see cref="FontAtlasFactory"/>.</param>
|
||||
/// <param name="assets">An instance of <see cref="DalamudAssetManager"/>.</param>
|
||||
public ChangelogWindow(
|
||||
TitleScreenMenuWindow tsmWindow,
|
||||
FontAtlasFactory fontAtlasFactory,
|
||||
DalamudAssetManager assets)
|
||||
: base("What's new in Dalamud?##ChangelogWindow", ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse, true)
|
||||
{
|
||||
this.tsmWindow = tsmWindow;
|
||||
this.Namespace = "DalamudChangelogWindow";
|
||||
this.privateAtlas = this.scopedFinalizer.Add(
|
||||
fontAtlasFactory.CreateFontAtlas(this.Namespace, FontAtlasAutoRebuildMode.Async));
|
||||
this.bannerFont = new(
|
||||
() => this.scopedFinalizer.Add(
|
||||
this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.MiedingerMid18))));
|
||||
|
||||
this.apiBumpExplainerTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.ChangelogApiBumpIcon));
|
||||
this.logoTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.Logo));
|
||||
|
||||
// If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch
|
||||
if (WarrantsChangelog())
|
||||
Service<GameFontManager>.GetAsync().ContinueWith(t => this.MakeFont(t.Result));
|
||||
_ = this.bannerFont.Value;
|
||||
}
|
||||
|
||||
private enum State
|
||||
|
|
@ -97,20 +113,12 @@ internal sealed class ChangelogWindow : Window, IDisposable
|
|||
Service<DalamudInterface>.Get().SetCreditsDarkeningAnimation(true);
|
||||
this.tsmWindow.AllowDrawing = false;
|
||||
|
||||
this.MakeFont(Service<GameFontManager>.Get());
|
||||
_ = this.bannerFont;
|
||||
|
||||
this.state = State.WindowFadeIn;
|
||||
this.windowFade.Reset();
|
||||
this.bodyFade.Reset();
|
||||
this.needFadeRestart = true;
|
||||
|
||||
if (this.apiBumpExplainerTexture == null)
|
||||
{
|
||||
var dalamud = Service<Dalamud>.Get();
|
||||
var tm = Service<TextureManager>.Get();
|
||||
this.apiBumpExplainerTexture = tm.GetTextureFromFile(new FileInfo(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "changelogApiBump.png")))
|
||||
?? throw new Exception("Could not load api bump explainer.");
|
||||
}
|
||||
|
||||
base.OnOpen();
|
||||
}
|
||||
|
|
@ -186,10 +194,7 @@ internal sealed class ChangelogWindow : Window, IDisposable
|
|||
ImGui.SetCursorPos(new Vector2(logoContainerSize.X / 2 - logoSize.X / 2, logoContainerSize.Y / 2 - logoSize.Y / 2));
|
||||
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f)))
|
||||
{
|
||||
this.logoTexture ??= Service<DalamudAssetManager>.Get().GetDalamudTextureWrap(DalamudAsset.Logo);
|
||||
ImGui.Image(this.logoTexture.ImGuiHandle, logoSize);
|
||||
}
|
||||
ImGui.Image(this.logoTexture.Value.ImGuiHandle, logoSize);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
|
@ -205,7 +210,7 @@ internal sealed class ChangelogWindow : Window, IDisposable
|
|||
|
||||
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 1f, 0f, 1f)))
|
||||
{
|
||||
using var font = ImRaii.PushFont(this.bannerFont!.ImFont);
|
||||
using var font = this.bannerFont.Value.Push();
|
||||
|
||||
switch (this.state)
|
||||
{
|
||||
|
|
@ -275,9 +280,11 @@ internal sealed class ChangelogWindow : Window, IDisposable
|
|||
ImGui.TextWrapped("If some plugins are displayed with a red cross in the 'Installed Plugins' tab, they may not yet be available.");
|
||||
|
||||
ImGuiHelpers.ScaledDummy(15);
|
||||
|
||||
ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture!.Width);
|
||||
ImGui.Image(this.apiBumpExplainerTexture.ImGuiHandle, this.apiBumpExplainerTexture.Size);
|
||||
|
||||
ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture.Value.Width);
|
||||
ImGui.Image(
|
||||
this.apiBumpExplainerTexture.Value.ImGuiHandle,
|
||||
this.apiBumpExplainerTexture.Value.Size);
|
||||
|
||||
DrawNextButton(State.Links);
|
||||
break;
|
||||
|
|
@ -377,7 +384,4 @@ internal sealed class ChangelogWindow : Window, IDisposable
|
|||
public void Dispose()
|
||||
{
|
||||
}
|
||||
|
||||
private void MakeFont(GameFontManager gfm) =>
|
||||
this.bannerFont ??= gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ using Dalamud.Interface.Components;
|
|||
using Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
using Serilog;
|
||||
|
||||
|
|
@ -14,7 +16,7 @@ namespace Dalamud.Interface.Internal.Windows.Data;
|
|||
/// <summary>
|
||||
/// Class responsible for drawing the data/debug window.
|
||||
/// </summary>
|
||||
internal class DataWindow : Window
|
||||
internal class DataWindow : Window, IDisposable
|
||||
{
|
||||
private readonly IDataWindowWidget[] modules =
|
||||
{
|
||||
|
|
@ -34,6 +36,7 @@ internal class DataWindow : Window
|
|||
new FlyTextWidget(),
|
||||
new FontAwesomeTestWidget(),
|
||||
new GameInventoryTestWidget(),
|
||||
new GamePrebakedFontsTestWidget(),
|
||||
new GamepadWidget(),
|
||||
new GaugeWidget(),
|
||||
new HookWidget(),
|
||||
|
|
@ -76,6 +79,9 @@ internal class DataWindow : Window
|
|||
this.Load();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose() => this.modules.OfType<IDisposable>().AggregateToDisposable().Dispose();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void OnOpen()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
using Dalamud.Game.Command;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.Game.Command;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
|
|
@ -28,9 +32,52 @@ internal class CommandWidget : IDataWindowWidget
|
|||
{
|
||||
var commandManager = Service<CommandManager>.Get();
|
||||
|
||||
foreach (var command in commandManager.Commands)
|
||||
var tableFlags = ImGuiTableFlags.ScrollY | ImGuiTableFlags.Borders | ImGuiTableFlags.SizingStretchProp |
|
||||
ImGuiTableFlags.Sortable | ImGuiTableFlags.SortTristate;
|
||||
using var table = ImRaii.Table("CommandList", 4, tableFlags);
|
||||
if (table)
|
||||
{
|
||||
ImGui.Text($"{command.Key}\n -> {command.Value.HelpMessage}\n -> In help: {command.Value.ShowInHelp}\n\n");
|
||||
ImGui.TableSetupScrollFreeze(0, 1);
|
||||
|
||||
ImGui.TableSetupColumn("Command");
|
||||
ImGui.TableSetupColumn("Plugin");
|
||||
ImGui.TableSetupColumn("HelpMessage", ImGuiTableColumnFlags.NoSort);
|
||||
ImGui.TableSetupColumn("In Help?", ImGuiTableColumnFlags.NoSort);
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
var sortSpecs = ImGui.TableGetSortSpecs();
|
||||
var commands = commandManager.Commands.ToArray();
|
||||
|
||||
if (sortSpecs.SpecsCount != 0)
|
||||
{
|
||||
commands = sortSpecs.Specs.ColumnIndex switch
|
||||
{
|
||||
0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
|
||||
? commands.OrderBy(kv => kv.Key).ToArray()
|
||||
: commands.OrderByDescending(kv => kv.Key).ToArray(),
|
||||
1 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending
|
||||
? commands.OrderBy(kv => kv.Value.LoaderAssemblyName).ToArray()
|
||||
: commands.OrderByDescending(kv => kv.Value.LoaderAssemblyName).ToArray(),
|
||||
_ => commands,
|
||||
};
|
||||
}
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
ImGui.TableNextRow();
|
||||
|
||||
ImGui.TableSetColumnIndex(0);
|
||||
ImGui.Text(command.Key);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.Text(command.Value.LoaderAssemblyName);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextWrapped(command.Value.HelpMessage);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.Text(command.Value.ShowInHelp ? "Yes" : "No");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,278 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
|
||||
|
||||
/// <summary>
|
||||
/// Widget for testing game prebaked fonts.
|
||||
/// </summary>
|
||||
internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable
|
||||
{
|
||||
private ImVectorWrapper<byte> testStringBuffer;
|
||||
private IFontAtlas? privateAtlas;
|
||||
private IReadOnlyDictionary<GameFontFamily, (GameFontStyle Size, Lazy<IFontHandle> Handle)[]>? fontHandles;
|
||||
private bool useGlobalScale;
|
||||
private bool useWordWrap;
|
||||
private bool useItalic;
|
||||
private bool useBold;
|
||||
private bool useMinimumBuild;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string[]? CommandShortcuts { get; init; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string DisplayName { get; init; } = "Game Prebaked Fonts";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Ready { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Load() => this.Ready = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe void Draw()
|
||||
{
|
||||
ImGui.AlignTextToFramePadding();
|
||||
fixed (byte* labelPtr = "Global Scale"u8)
|
||||
{
|
||||
var v = (byte)(this.useGlobalScale ? 1 : 0);
|
||||
if (ImGuiNative.igCheckbox(labelPtr, &v) != 0)
|
||||
{
|
||||
this.useGlobalScale = v != 0;
|
||||
this.ClearAtlas();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
fixed (byte* labelPtr = "Word Wrap"u8)
|
||||
{
|
||||
var v = (byte)(this.useWordWrap ? 1 : 0);
|
||||
if (ImGuiNative.igCheckbox(labelPtr, &v) != 0)
|
||||
this.useWordWrap = v != 0;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
fixed (byte* labelPtr = "Italic"u8)
|
||||
{
|
||||
var v = (byte)(this.useItalic ? 1 : 0);
|
||||
if (ImGuiNative.igCheckbox(labelPtr, &v) != 0)
|
||||
{
|
||||
this.useItalic = v != 0;
|
||||
this.ClearAtlas();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
fixed (byte* labelPtr = "Bold"u8)
|
||||
{
|
||||
var v = (byte)(this.useBold ? 1 : 0);
|
||||
if (ImGuiNative.igCheckbox(labelPtr, &v) != 0)
|
||||
{
|
||||
this.useBold = v != 0;
|
||||
this.ClearAtlas();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
fixed (byte* labelPtr = "Minimum Range"u8)
|
||||
{
|
||||
var v = (byte)(this.useMinimumBuild ? 1 : 0);
|
||||
if (ImGuiNative.igCheckbox(labelPtr, &v) != 0)
|
||||
{
|
||||
this.useMinimumBuild = v != 0;
|
||||
this.ClearAtlas();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Reset Text") || this.testStringBuffer.IsDisposed)
|
||||
{
|
||||
this.testStringBuffer.Dispose();
|
||||
this.testStringBuffer = ImVectorWrapper.CreateFromSpan(
|
||||
"(Game)-[Font] {Test}. 0123456789!! <氣気气きキ기>。"u8,
|
||||
minCapacity: 1024);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Test Lock"))
|
||||
Task.Run(this.TestLock);
|
||||
|
||||
fixed (byte* labelPtr = "Test Input"u8)
|
||||
{
|
||||
if (ImGuiNative.igInputTextMultiline(
|
||||
labelPtr,
|
||||
this.testStringBuffer.Data,
|
||||
(uint)this.testStringBuffer.Capacity,
|
||||
new(ImGui.GetContentRegionAvail().X, 32 * ImGuiHelpers.GlobalScale),
|
||||
0,
|
||||
null,
|
||||
null) != 0)
|
||||
{
|
||||
var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0);
|
||||
if (len + 4 >= this.testStringBuffer.Capacity)
|
||||
this.testStringBuffer.EnsureCapacityExponential(len + 4);
|
||||
if (len < this.testStringBuffer.Capacity)
|
||||
{
|
||||
this.testStringBuffer.LengthUnsafe = len;
|
||||
this.testStringBuffer.StorageSpan[len] = default;
|
||||
}
|
||||
|
||||
if (this.useMinimumBuild)
|
||||
_ = this.privateAtlas?.BuildFontsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
this.privateAtlas ??=
|
||||
Service<FontAtlasFactory>.Get().CreateFontAtlas(
|
||||
nameof(GamePrebakedFontsTestWidget),
|
||||
FontAtlasAutoRebuildMode.Async,
|
||||
this.useGlobalScale);
|
||||
this.fontHandles ??=
|
||||
Enum.GetValues<GameFontFamilyAndSize>()
|
||||
.Where(x => x.GetAttribute<GameFontFamilyAndSizeAttribute>() is not null)
|
||||
.Select(x => new GameFontStyle(x) { Italic = this.useItalic, Bold = this.useBold })
|
||||
.GroupBy(x => x.Family)
|
||||
.ToImmutableDictionary(
|
||||
x => x.Key,
|
||||
x => x.Select(
|
||||
y => (y, new Lazy<IFontHandle>(
|
||||
() => this.useMinimumBuild
|
||||
? this.privateAtlas.NewDelegateFontHandle(
|
||||
e =>
|
||||
e.OnPreBuild(
|
||||
tk => tk.AddGameGlyphs(
|
||||
y,
|
||||
Encoding.UTF8.GetString(
|
||||
this.testStringBuffer.DataSpan).ToGlyphRange(),
|
||||
default)))
|
||||
: this.privateAtlas.NewGameFontHandle(y))))
|
||||
.ToArray());
|
||||
|
||||
var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2);
|
||||
var counter = 0;
|
||||
foreach (var (family, items) in this.fontHandles)
|
||||
{
|
||||
if (!ImGui.CollapsingHeader($"{family} Family"))
|
||||
continue;
|
||||
|
||||
foreach (var (gfs, handle) in items)
|
||||
{
|
||||
ImGui.TextUnformatted($"{gfs.SizePt}pt");
|
||||
ImGui.SameLine(offsetX);
|
||||
ImGuiNative.igPushTextWrapPos(this.useWordWrap ? 0f : -1f);
|
||||
try
|
||||
{
|
||||
if (handle.Value.LoadException is { } exc)
|
||||
{
|
||||
ImGui.TextUnformatted(exc.ToString());
|
||||
}
|
||||
else if (!handle.Value.Available)
|
||||
{
|
||||
fixed (byte* labelPtr = "Loading..."u8)
|
||||
ImGuiNative.igTextUnformatted(labelPtr, labelPtr + 8 + ((Environment.TickCount / 200) % 3));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!this.useGlobalScale)
|
||||
ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale);
|
||||
if (counter++ % 2 == 0)
|
||||
{
|
||||
using var pushPop = handle.Value.Push();
|
||||
ImGuiNative.igTextUnformatted(
|
||||
this.testStringBuffer.Data,
|
||||
this.testStringBuffer.Data + this.testStringBuffer.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
handle.Value.Push();
|
||||
ImGuiNative.igTextUnformatted(
|
||||
this.testStringBuffer.Data,
|
||||
this.testStringBuffer.Data + this.testStringBuffer.Length);
|
||||
handle.Value.Pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ImGuiNative.igPopTextWrapPos();
|
||||
ImGuiNative.igSetWindowFontScale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.ClearAtlas();
|
||||
this.testStringBuffer.Dispose();
|
||||
}
|
||||
|
||||
private void ClearAtlas()
|
||||
{
|
||||
this.fontHandles?.Values.SelectMany(x => x.Where(y => y.Handle.IsValueCreated).Select(y => y.Handle.Value))
|
||||
.AggregateToDisposable().Dispose();
|
||||
this.fontHandles = null;
|
||||
this.privateAtlas?.Dispose();
|
||||
this.privateAtlas = null;
|
||||
}
|
||||
|
||||
private async void TestLock()
|
||||
{
|
||||
if (this.fontHandles is not { } fontHandlesCopy)
|
||||
return;
|
||||
|
||||
Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} waiting for build");
|
||||
|
||||
await using var garbage = new DisposeSafety.ScopedFinalizer();
|
||||
var fonts = new List<ImFontPtr>();
|
||||
IFontHandle[] handles;
|
||||
try
|
||||
{
|
||||
handles = fontHandlesCopy.Values.SelectMany(x => x).Select(x => x.Handle.Value).ToArray();
|
||||
foreach (var handle in handles)
|
||||
{
|
||||
await handle.WaitAsync();
|
||||
var locked = handle.Lock();
|
||||
garbage.Add(locked);
|
||||
fonts.Add(locked.ImFont);
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} waiting in lock");
|
||||
await Task.Delay(5000);
|
||||
|
||||
foreach (var (font, handle) in fonts.Zip(handles))
|
||||
TestSingle(font, handle);
|
||||
|
||||
return;
|
||||
|
||||
unsafe void TestSingle(ImFontPtr fontPtr, IFontHandle handle)
|
||||
{
|
||||
var dim = default(Vector2);
|
||||
var test = "Test string"u8;
|
||||
fixed (byte* pTest = test)
|
||||
ImGuiNative.ImFont_CalcTextSizeA(&dim, fontPtr, fontPtr.FontSize, float.MaxValue, 0, pTest, null, null);
|
||||
Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {handle} => {dim}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -98,6 +98,12 @@ internal class PluginImageCache : IDisposable, IServiceType
|
|||
/// </summary>
|
||||
public IDalamudTextureWrap TroubleIcon =>
|
||||
this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TroubleIcon, this.EmptyTexture);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the devPlugin icon overlay.
|
||||
/// </summary>
|
||||
public IDalamudTextureWrap DevPluginIcon =>
|
||||
this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon, this.EmptyTexture);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin update icon overlay.
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
private int updatePluginCount = 0;
|
||||
private List<PluginUpdateStatus>? updatedPlugins;
|
||||
|
||||
[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Makes sense like this")]
|
||||
private List<RemotePluginManifest> pluginListAvailable = new();
|
||||
private List<LocalPlugin> pluginListInstalled = new();
|
||||
private List<AvailablePluginUpdate> pluginListUpdatable = new();
|
||||
|
|
@ -1126,45 +1127,79 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
this.DrawChangelog(logEntry);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private record PluginInstallerAvailablePluginProxy(RemotePluginManifest? RemoteManifest, LocalPlugin? LocalPlugin);
|
||||
|
||||
#pragma warning disable SA1201
|
||||
private void DrawAvailablePluginList()
|
||||
#pragma warning restore SA1201
|
||||
{
|
||||
var pluginList = this.pluginListAvailable;
|
||||
var availableManifests = this.pluginListAvailable;
|
||||
var installedPlugins = this.pluginListInstalled.ToList(); // Copy intended
|
||||
|
||||
if (pluginList.Count == 0)
|
||||
if (availableManifests.Count == 0)
|
||||
{
|
||||
ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoCompatible);
|
||||
return;
|
||||
}
|
||||
|
||||
var filteredManifests = pluginList
|
||||
var filteredAvailableManifests = availableManifests
|
||||
.Where(rm => !this.IsManifestFiltered(rm))
|
||||
.ToList();
|
||||
|
||||
if (filteredManifests.Count == 0)
|
||||
if (filteredAvailableManifests.Count == 0)
|
||||
{
|
||||
ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching);
|
||||
return;
|
||||
}
|
||||
|
||||
// get list to show and reset category dirty flag
|
||||
var categoryManifestsList = this.categoryManager.GetCurrentCategoryContent(filteredManifests);
|
||||
var proxies = new List<PluginInstallerAvailablePluginProxy>();
|
||||
|
||||
// Go through all AVAILABLE manifests, associate them with a NON-DEV local plugin, if one is available, and remove it from the pile
|
||||
foreach (var availableManifest in this.categoryManager.GetCurrentCategoryContent(filteredAvailableManifests).Cast<RemotePluginManifest>())
|
||||
{
|
||||
var plugin = this.pluginListInstalled.FirstOrDefault(plugin => plugin.Manifest.InternalName == availableManifest.InternalName && plugin.Manifest.RepoUrl == availableManifest.RepoUrl);
|
||||
|
||||
// We "consumed" this plugin from the pile and remove it.
|
||||
if (plugin != null && !plugin.IsDev)
|
||||
{
|
||||
installedPlugins.Remove(plugin);
|
||||
proxies.Add(new PluginInstallerAvailablePluginProxy(null, plugin));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
proxies.Add(new PluginInstallerAvailablePluginProxy(availableManifest, null));
|
||||
}
|
||||
|
||||
// Now, add all applicable local plugins that haven't been "used up", in most cases either dev or orphaned plugins.
|
||||
foreach (var installedPlugin in installedPlugins)
|
||||
{
|
||||
if (this.IsManifestFiltered(installedPlugin.Manifest))
|
||||
continue;
|
||||
|
||||
// TODO: We should also check categories here, for good measure
|
||||
|
||||
proxies.Add(new PluginInstallerAvailablePluginProxy(null, installedPlugin));
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
foreach (var manifest in categoryManifestsList)
|
||||
foreach (var proxy in proxies)
|
||||
{
|
||||
if (manifest is not RemotePluginManifest remoteManifest)
|
||||
continue;
|
||||
var (isInstalled, plugin) = this.IsManifestInstalled(remoteManifest);
|
||||
IPluginManifest applicableManifest = proxy.LocalPlugin != null ? proxy.LocalPlugin.Manifest : proxy.RemoteManifest;
|
||||
|
||||
ImGui.PushID($"{manifest.InternalName}{manifest.AssemblyVersion}");
|
||||
if (isInstalled)
|
||||
if (applicableManifest == null)
|
||||
throw new Exception("Could not determine manifest for available plugin");
|
||||
|
||||
ImGui.PushID($"{applicableManifest.InternalName}{applicableManifest.AssemblyVersion}");
|
||||
|
||||
if (proxy.LocalPlugin != null)
|
||||
{
|
||||
this.DrawInstalledPlugin(plugin, i++, true);
|
||||
this.DrawInstalledPlugin(proxy.LocalPlugin, i++, true);
|
||||
}
|
||||
else
|
||||
else if (proxy.RemoteManifest != null)
|
||||
{
|
||||
this.DrawAvailablePlugin(remoteManifest, i++);
|
||||
this.DrawAvailablePlugin(proxy.RemoteManifest, i++);
|
||||
}
|
||||
|
||||
ImGui.PopID();
|
||||
|
|
@ -1828,8 +1863,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
// Name
|
||||
ImGui.TextUnformatted(label);
|
||||
|
||||
// Verified Checkmark, don't show for dev plugins
|
||||
if (plugin is null or { IsDev: false })
|
||||
// Verified Checkmark or dev plugin wrench
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.Text(" ");
|
||||
|
|
@ -1839,8 +1873,15 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
var unverifiedOutlineColor = KnownColor.Black.Vector();
|
||||
var verifiedIconColor = KnownColor.RoyalBlue.Vector() with { W = 0.75f };
|
||||
var unverifiedIconColor = KnownColor.Orange.Vector();
|
||||
|
||||
if (!isThirdParty)
|
||||
var devIconOutlineColor = KnownColor.White.Vector();
|
||||
var devIconColor = KnownColor.MediumOrchid.Vector();
|
||||
|
||||
if (plugin is LocalDevPlugin)
|
||||
{
|
||||
this.DrawFontawesomeIconOutlined(FontAwesomeIcon.Wrench, devIconOutlineColor, devIconColor);
|
||||
this.VerifiedCheckmarkFadeTooltip(label, "This is a dev plugin. You added it.");
|
||||
}
|
||||
else if (!isThirdParty)
|
||||
{
|
||||
this.DrawFontawesomeIconOutlined(FontAwesomeIcon.CheckCircle, verifiedOutlineColor, verifiedIconColor);
|
||||
this.VerifiedCheckmarkFadeTooltip(label, Locs.VerifiedCheckmark_VerifiedTooltip);
|
||||
|
|
@ -1873,16 +1914,32 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
if (plugin is { IsOutdated: true, IsBanned: false } || installableOutdated)
|
||||
{
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
|
||||
ImGui.TextWrapped(Locs.PluginBody_Outdated);
|
||||
|
||||
var bodyText = Locs.PluginBody_Outdated + " ";
|
||||
if (updateAvailable)
|
||||
bodyText += Locs.PluginBody_Outdated_CanNowUpdate;
|
||||
else
|
||||
bodyText += Locs.PluginBody_Outdated_WaitForUpdate;
|
||||
|
||||
ImGui.TextWrapped(bodyText);
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
else if (plugin is { IsBanned: true })
|
||||
{
|
||||
// Banned warning
|
||||
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
|
||||
ImGuiHelpers.SafeTextWrapped(plugin.BanReason.IsNullOrEmpty()
|
||||
? Locs.PluginBody_Banned
|
||||
: Locs.PluginBody_BannedReason(plugin.BanReason));
|
||||
|
||||
var bodyText = plugin.BanReason.IsNullOrEmpty()
|
||||
? Locs.PluginBody_Banned
|
||||
: Locs.PluginBody_BannedReason(plugin.BanReason);
|
||||
bodyText += " ";
|
||||
|
||||
if (updateAvailable)
|
||||
bodyText += Locs.PluginBody_Outdated_CanNowUpdate;
|
||||
else
|
||||
bodyText += Locs.PluginBody_Outdated_WaitForUpdate;
|
||||
|
||||
ImGuiHelpers.SafeTextWrapped(bodyText);
|
||||
|
||||
ImGui.PopStyleColor();
|
||||
}
|
||||
|
|
@ -2238,6 +2295,11 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
}
|
||||
|
||||
var availablePluginUpdate = this.pluginListUpdatable.FirstOrDefault(up => up.InstalledPlugin == plugin);
|
||||
|
||||
// Dev plugins can never update
|
||||
if (plugin.IsDev)
|
||||
availablePluginUpdate = null;
|
||||
|
||||
// Update available
|
||||
if (availablePluginUpdate != default)
|
||||
{
|
||||
|
|
@ -2494,7 +2556,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
|
||||
if (ImGui.MenuItem(Locs.PluginContext_DeletePluginConfigReload))
|
||||
{
|
||||
this.ShowDeletePluginConfigWarningModal(plugin.Name).ContinueWith(t =>
|
||||
this.ShowDeletePluginConfigWarningModal(plugin.Manifest.Name).ContinueWith(t =>
|
||||
{
|
||||
var shouldDelete = t.Result;
|
||||
|
||||
|
|
@ -2509,7 +2571,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
{
|
||||
this.installStatus = OperationStatus.Idle;
|
||||
|
||||
this.DisplayErrorContinuation(task, Locs.ErrorModal_DeleteConfigFail(plugin.Name));
|
||||
this.DisplayErrorContinuation(task, Locs.ErrorModal_DeleteConfigFail(plugin.Manifest.InternalName));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -2526,12 +2588,12 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
var profileManager = Service<ProfileManager>.Get();
|
||||
var config = Service<DalamudConfiguration>.Get();
|
||||
|
||||
var applicableForProfiles = plugin.Manifest.SupportsProfiles && !plugin.IsDev;
|
||||
var applicableForProfiles = plugin.Manifest.SupportsProfiles /*&& !plugin.IsDev*/;
|
||||
var profilesThatWantThisPlugin = profileManager.Profiles
|
||||
.Where(x => x.WantsPlugin(plugin.InternalName) != null)
|
||||
.Where(x => x.WantsPlugin(plugin.Manifest.WorkingPluginId) != null)
|
||||
.ToArray();
|
||||
var isInSingleProfile = profilesThatWantThisPlugin.Length == 1;
|
||||
var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.InternalName);
|
||||
var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.WorkingPluginId);
|
||||
|
||||
// Disable everything if the updater is running or another plugin is operating
|
||||
var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress;
|
||||
|
|
@ -2566,17 +2628,17 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
|
||||
foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile))
|
||||
{
|
||||
var inProfile = profile.WantsPlugin(plugin.Manifest.InternalName) != null;
|
||||
var inProfile = profile.WantsPlugin(plugin.Manifest.WorkingPluginId) != null;
|
||||
if (ImGui.Checkbox($"###profilePick{profile.Guid}{plugin.Manifest.InternalName}", ref inProfile))
|
||||
{
|
||||
if (inProfile)
|
||||
{
|
||||
Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.InternalName, true))
|
||||
Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true))
|
||||
.ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotAdd);
|
||||
}
|
||||
else
|
||||
{
|
||||
Task.Run(() => profile.RemoveAsync(plugin.Manifest.InternalName))
|
||||
Task.Run(() => profile.RemoveAsync(plugin.Manifest.WorkingPluginId))
|
||||
.ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotRemove);
|
||||
}
|
||||
}
|
||||
|
|
@ -2596,11 +2658,11 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
|
||||
{
|
||||
// TODO: Work this out
|
||||
Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, plugin.IsLoaded, false))
|
||||
Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, plugin.IsLoaded, false))
|
||||
.GetAwaiter().GetResult();
|
||||
foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile && x.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName)))
|
||||
{
|
||||
Task.Run(() => profile.RemoveAsync(plugin.Manifest.InternalName, false))
|
||||
Task.Run(() => profile.RemoveAsync(plugin.Manifest.WorkingPluginId, false))
|
||||
.GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
|
|
@ -2674,7 +2736,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
{
|
||||
await plugin.UnloadAsync();
|
||||
await applicableProfile.AddOrUpdateAsync(
|
||||
plugin.Manifest.InternalName, false, false);
|
||||
plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false);
|
||||
|
||||
notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success);
|
||||
}).ContinueWith(t =>
|
||||
|
|
@ -2691,7 +2753,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
this.loadingIndicatorKind = LoadingIndicatorKind.EnablingSingle;
|
||||
this.enableDisableWorkingPluginId = plugin.Manifest.WorkingPluginId;
|
||||
|
||||
await applicableProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false);
|
||||
await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false);
|
||||
await plugin.LoadAsync(PluginLoadReason.Installer);
|
||||
|
||||
notifications.AddNotification(Locs.Notifications_PluginEnabled(plugin.Manifest.Name), Locs.Notifications_PluginEnabledTitle, NotificationType.Success);
|
||||
|
|
@ -2712,7 +2774,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
if (shouldUpdate)
|
||||
{
|
||||
// We need to update the profile right here, because PM will not enable the plugin otherwise
|
||||
await applicableProfile.AddOrUpdateAsync(plugin.InternalName, true, false);
|
||||
await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false);
|
||||
await this.UpdateSinglePlugin(availableUpdate);
|
||||
}
|
||||
else
|
||||
|
|
@ -2893,7 +2955,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
if (localPlugin is LocalDevPlugin plugin)
|
||||
{
|
||||
var isInDefaultProfile =
|
||||
Service<ProfileManager>.Get().IsInDefaultProfile(localPlugin.Manifest.InternalName);
|
||||
Service<ProfileManager>.Get().IsInDefaultProfile(localPlugin.Manifest.WorkingPluginId);
|
||||
|
||||
// https://colorswall.com/palette/2868/
|
||||
var greenColor = new Vector4(0x5C, 0xB8, 0x5C, 0xFF) / 0xFF;
|
||||
|
|
@ -3237,7 +3299,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
this.pluginListAvailable.Sort((p1, p2) => p1.Name.CompareTo(p2.Name));
|
||||
|
||||
var profman = Service<ProfileManager>.Get();
|
||||
this.pluginListInstalled.Sort((p1, p2) => profman.IsInDefaultProfile(p1.InternalName).CompareTo(profman.IsInDefaultProfile(p2.InternalName)));
|
||||
this.pluginListInstalled.Sort((p1, p2) => profman.IsInDefaultProfile(p1.Manifest.WorkingPluginId).CompareTo(profman.IsInDefaultProfile(p2.Manifest.WorkingPluginId)));
|
||||
break;
|
||||
default:
|
||||
throw new InvalidEnumArgumentException("Unknown plugin sort type.");
|
||||
|
|
@ -3484,7 +3546,11 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
|
||||
public static string PluginBody_Plugin3rdPartyRepo(string url) => Loc.Localize("InstallerPlugin3rdPartyRepo", "From custom plugin repository {0}").Format(url);
|
||||
|
||||
public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible at the moment. Please wait for it to be updated by its author.");
|
||||
public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible.");
|
||||
|
||||
public static string PluginBody_Outdated_WaitForUpdate => Loc.Localize("InstallerOutdatedWaitForUpdate", "Please wait for it to be updated by its author.");
|
||||
|
||||
public static string PluginBody_Outdated_CanNowUpdate => Loc.Localize("InstallerOutdatedCanNowUpdate", "An update is available for installation.");
|
||||
|
||||
public static string PluginBody_Orphaned => Loc.Localize("InstallerOrphanedPluginBody ", "This plugin's source repository is no longer available. You may need to reinstall it from its repository, or re-add the repository.");
|
||||
|
||||
|
|
@ -3494,7 +3560,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
|
||||
public static string PluginBody_LoadFailed => Loc.Localize("InstallerLoadFailedPluginBody ", "This plugin failed to load. Please contact the author for more information.");
|
||||
|
||||
public static string PluginBody_Banned => Loc.Localize("InstallerBannedPluginBody ", "This plugin was automatically disabled due to incompatibilities and is not available at the moment. Please wait for it to be updated by its author.");
|
||||
public static string PluginBody_Banned => Loc.Localize("InstallerBannedPluginBody ", "This plugin was automatically disabled due to incompatibilities and is not available.");
|
||||
|
||||
public static string PluginBody_Policy => Loc.Localize("InstallerPolicyPluginBody ", "Plugin loads for this type of plugin were manually disabled.");
|
||||
|
||||
|
|
@ -3707,7 +3773,7 @@ internal class PluginInstallerWindow : Window, IDisposable
|
|||
|
||||
public static string DeletePluginConfigWarningModal_Title => Loc.Localize("InstallerDeletePluginConfigWarning", "Warning###InstallerDeletePluginConfigWarning");
|
||||
|
||||
public static string DeletePluginConfigWarningModal_Body(string pluginName) => Loc.Localize("InstallerDeletePluginConfigWarningBody", "Are you sure you want to delete all data and configuration for v{0}?").Format(pluginName);
|
||||
public static string DeletePluginConfigWarningModal_Body(string pluginName) => Loc.Localize("InstallerDeletePluginConfigWarningBody", "Are you sure you want to delete all data and configuration for {0}?").Format(pluginName);
|
||||
|
||||
public static string DeletePluginConfirmWarningModal_Yes => Loc.Localize("InstallerDeletePluginConfigWarningYes", "Yes");
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using Dalamud.Interface.Utility;
|
|||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Plugin.Internal.Profiles;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Utility;
|
||||
using ImGuiNET;
|
||||
using Serilog;
|
||||
|
|
@ -252,7 +253,7 @@ internal class ProfileManagerWidget
|
|||
|
||||
if (ImGuiComponents.IconButton($"###exportButton{profile.Guid}", FontAwesomeIcon.FileExport))
|
||||
{
|
||||
ImGui.SetClipboardText(profile.Model.Serialize());
|
||||
ImGui.SetClipboardText(profile.Model.SerializeForShare());
|
||||
Service<NotificationManager>.Get().AddNotification(Locs.CopyToClipboardNotification, type: NotificationType.Success);
|
||||
}
|
||||
|
||||
|
|
@ -315,15 +316,15 @@ internal class ProfileManagerWidget
|
|||
if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80)))
|
||||
{
|
||||
// TODO: Plugin searching should be abstracted... installer and this should use the same search
|
||||
foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles && !x.IsDev &&
|
||||
foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles &&
|
||||
(this.pickerSearch.IsNullOrWhitespace() || x.Manifest.Name.ToLowerInvariant().Contains(this.pickerSearch.ToLowerInvariant()))))
|
||||
{
|
||||
using var disabled2 =
|
||||
ImRaii.Disabled(profile.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName));
|
||||
|
||||
if (ImGui.Selectable($"{plugin.Manifest.Name}###selector{plugin.Manifest.InternalName}"))
|
||||
if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}"))
|
||||
{
|
||||
Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false))
|
||||
Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false))
|
||||
.ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
|
||||
}
|
||||
}
|
||||
|
|
@ -350,7 +351,7 @@ internal class ProfileManagerWidget
|
|||
|
||||
if (ImGuiComponents.IconButton(FontAwesomeIcon.FileExport))
|
||||
{
|
||||
ImGui.SetClipboardText(profile.Model.Serialize());
|
||||
ImGui.SetClipboardText(profile.Model.SerializeForShare());
|
||||
Service<NotificationManager>.Get().AddNotification(Locs.CopyToClipboardNotification, type: NotificationType.Success);
|
||||
}
|
||||
|
||||
|
|
@ -423,24 +424,34 @@ internal class ProfileManagerWidget
|
|||
if (pluginListChild)
|
||||
{
|
||||
var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale;
|
||||
string? wantRemovePluginInternalName = null;
|
||||
Guid? wantRemovePluginGuid = null;
|
||||
|
||||
using var syncScope = profile.GetSyncScope();
|
||||
foreach (var plugin in profile.Plugins.ToArray())
|
||||
foreach (var profileEntry in profile.Plugins.ToArray())
|
||||
{
|
||||
didAny = true;
|
||||
var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == plugin.InternalName);
|
||||
var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.WorkingPluginId == profileEntry.WorkingPluginId);
|
||||
var btnOffset = 2;
|
||||
|
||||
if (pmPlugin != null)
|
||||
{
|
||||
var cursorBeforeIcon = ImGui.GetCursorPos();
|
||||
pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.IsThirdParty, out var icon);
|
||||
icon ??= pic.DefaultIcon;
|
||||
|
||||
ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight));
|
||||
|
||||
if (pmPlugin.IsDev)
|
||||
{
|
||||
ImGui.SetCursorPos(cursorBeforeIcon);
|
||||
ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.7f);
|
||||
ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight));
|
||||
ImGui.PopStyleVar();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
var text = $"{pmPlugin.Name}";
|
||||
var text = $"{pmPlugin.Name}{(pmPlugin.IsDev ? " (dev plugin" : string.Empty)}";
|
||||
var textHeight = ImGui.CalcTextSize(text);
|
||||
var before = ImGui.GetCursorPos();
|
||||
|
||||
|
|
@ -454,32 +465,53 @@ internal class ProfileManagerWidget
|
|||
ImGui.Image(pic.DefaultIcon.ImGuiHandle, new Vector2(pluginLineHeight));
|
||||
ImGui.SameLine();
|
||||
|
||||
var text = Locs.NotInstalled(plugin.InternalName);
|
||||
var text = Locs.NotInstalled(profileEntry.InternalName);
|
||||
var textHeight = ImGui.CalcTextSize(text);
|
||||
var before = ImGui.GetCursorPos();
|
||||
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2));
|
||||
ImGui.TextUnformatted(text);
|
||||
|
||||
var available =
|
||||
|
||||
var firstAvailableInstalled = pm.InstalledPlugins.FirstOrDefault(x => x.InternalName == profileEntry.InternalName);
|
||||
var installable =
|
||||
pm.AvailablePlugins.FirstOrDefault(
|
||||
x => x.InternalName == plugin.InternalName && !x.SourceRepo.IsThirdParty);
|
||||
if (available != null)
|
||||
x => x.InternalName == profileEntry.InternalName && !x.SourceRepo.IsThirdParty);
|
||||
|
||||
if (firstAvailableInstalled != null)
|
||||
{
|
||||
ImGui.Text($"Match to plugin '{firstAvailableInstalled.Name}'?");
|
||||
ImGui.SameLine();
|
||||
if (ImGuiComponents.IconButtonWithText(
|
||||
FontAwesomeIcon.Check,
|
||||
"Yes, use this one"))
|
||||
{
|
||||
profileEntry.WorkingPluginId = firstAvailableInstalled.Manifest.WorkingPluginId;
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await profman.ApplyAllWantStatesAsync();
|
||||
})
|
||||
.ContinueWith(t =>
|
||||
{
|
||||
this.installer.DisplayErrorContinuation(t, Locs.ErrorCouldNotChangeState);
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (installable != null)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * 2) - 2);
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2));
|
||||
btnOffset = 3;
|
||||
|
||||
if (ImGuiComponents.IconButton($"###installMissingPlugin{available.InternalName}", FontAwesomeIcon.Download))
|
||||
if (ImGuiComponents.IconButton($"###installMissingPlugin{installable.InternalName}", FontAwesomeIcon.Download))
|
||||
{
|
||||
this.installer.StartInstall(available, false);
|
||||
this.installer.StartInstall(installable, false);
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(Locs.InstallPlugin);
|
||||
}
|
||||
|
||||
|
||||
ImGui.SetCursorPos(before);
|
||||
}
|
||||
|
||||
|
|
@ -487,10 +519,10 @@ internal class ProfileManagerWidget
|
|||
ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30));
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2));
|
||||
|
||||
var enabled = plugin.IsEnabled;
|
||||
if (ImGui.Checkbox($"###{this.editingProfileGuid}-{plugin.InternalName}", ref enabled))
|
||||
var enabled = profileEntry.IsEnabled;
|
||||
if (ImGui.Checkbox($"###{this.editingProfileGuid}-{profileEntry.InternalName}", ref enabled))
|
||||
{
|
||||
Task.Run(() => profile.AddOrUpdateAsync(plugin.InternalName, enabled))
|
||||
Task.Run(() => profile.AddOrUpdateAsync(profileEntry.WorkingPluginId, profileEntry.InternalName, enabled))
|
||||
.ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState);
|
||||
}
|
||||
|
||||
|
|
@ -498,19 +530,19 @@ internal class ProfileManagerWidget
|
|||
ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * btnOffset) - 5);
|
||||
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2));
|
||||
|
||||
if (ImGuiComponents.IconButton($"###removePlugin{plugin.InternalName}", FontAwesomeIcon.Trash))
|
||||
if (ImGuiComponents.IconButton($"###removePlugin{profileEntry.InternalName}", FontAwesomeIcon.Trash))
|
||||
{
|
||||
wantRemovePluginInternalName = plugin.InternalName;
|
||||
wantRemovePluginGuid = profileEntry.WorkingPluginId;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(Locs.RemovePlugin);
|
||||
}
|
||||
|
||||
if (wantRemovePluginInternalName != null)
|
||||
if (wantRemovePluginGuid != null)
|
||||
{
|
||||
// TODO: handle error
|
||||
Task.Run(() => profile.RemoveAsync(wantRemovePluginInternalName, false))
|
||||
Task.Run(() => profile.RemoveAsync(wantRemovePluginGuid.Value, false))
|
||||
.ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotRemove);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ using CheapLoc;
|
|||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Internal.Windows.Settings.Tabs;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Utility;
|
||||
using ImGuiNET;
|
||||
|
||||
|
|
@ -19,14 +19,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings;
|
|||
/// </summary>
|
||||
internal class SettingsWindow : Window
|
||||
{
|
||||
private readonly SettingsTab[] tabs =
|
||||
{
|
||||
new SettingsTabGeneral(),
|
||||
new SettingsTabLook(),
|
||||
new SettingsTabDtr(),
|
||||
new SettingsTabExperimental(),
|
||||
new SettingsTabAbout(),
|
||||
};
|
||||
private SettingsTab[]? tabs;
|
||||
|
||||
private string searchInput = string.Empty;
|
||||
|
||||
|
|
@ -49,6 +42,15 @@ internal class SettingsWindow : Window
|
|||
/// <inheritdoc/>
|
||||
public override void OnOpen()
|
||||
{
|
||||
this.tabs ??= new SettingsTab[]
|
||||
{
|
||||
new SettingsTabGeneral(),
|
||||
new SettingsTabLook(),
|
||||
new SettingsTabDtr(),
|
||||
new SettingsTabExperimental(),
|
||||
new SettingsTabAbout(),
|
||||
};
|
||||
|
||||
foreach (var settingsTab in this.tabs)
|
||||
{
|
||||
settingsTab.Load();
|
||||
|
|
@ -64,15 +66,13 @@ internal class SettingsWindow : Window
|
|||
{
|
||||
var configuration = Service<DalamudConfiguration>.Get();
|
||||
var interfaceManager = Service<InterfaceManager>.Get();
|
||||
var fontAtlasFactory = Service<FontAtlasFactory>.Get();
|
||||
|
||||
var rebuildFont =
|
||||
ImGui.GetIO().FontGlobalScale != configuration.GlobalUiScale ||
|
||||
interfaceManager.FontGamma != configuration.FontGammaLevel ||
|
||||
interfaceManager.UseAxis != configuration.UseAxisFontsFromGame;
|
||||
var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame;
|
||||
rebuildFont |= !Equals(ImGui.GetIO().FontGlobalScale, configuration.GlobalUiScale);
|
||||
|
||||
ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale;
|
||||
interfaceManager.FontGammaOverride = null;
|
||||
interfaceManager.UseAxisOverride = null;
|
||||
fontAtlasFactory.UseAxisOverride = null;
|
||||
|
||||
if (rebuildFont)
|
||||
interfaceManager.RebuildFonts();
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
using CheapLoc;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Plugin.Internal;
|
||||
|
|
@ -15,7 +15,6 @@ using Dalamud.Storage.Assets;
|
|||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
||||
using ImGuiNET;
|
||||
using ImGuiScene;
|
||||
|
||||
namespace Dalamud.Interface.Internal.Windows.Settings.Tabs;
|
||||
|
||||
|
|
@ -173,16 +172,21 @@ Contribute at: https://github.com/goatcorp/Dalamud
|
|||
";
|
||||
|
||||
private readonly Stopwatch creditsThrottler;
|
||||
private readonly IFontAtlas privateAtlas;
|
||||
|
||||
private string creditsText;
|
||||
|
||||
private bool resetNow = false;
|
||||
private IDalamudTextureWrap? logoTexture;
|
||||
private GameFontHandle? thankYouFont;
|
||||
private IFontHandle? thankYouFont;
|
||||
|
||||
public SettingsTabAbout()
|
||||
{
|
||||
this.creditsThrottler = new();
|
||||
|
||||
this.privateAtlas = Service<FontAtlasFactory>
|
||||
.Get()
|
||||
.CreateFontAtlas(nameof(SettingsTabAbout), FontAtlasAutoRebuildMode.Async);
|
||||
}
|
||||
|
||||
public override SettingsEntry[] Entries { get; } = { };
|
||||
|
|
@ -207,11 +211,7 @@ Contribute at: https://github.com/goatcorp/Dalamud
|
|||
|
||||
this.creditsThrottler.Restart();
|
||||
|
||||
if (this.thankYouFont == null)
|
||||
{
|
||||
var gfm = Service<GameFontManager>.Get();
|
||||
this.thankYouFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.TrumpGothic34));
|
||||
}
|
||||
this.thankYouFont ??= this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.TrumpGothic34));
|
||||
|
||||
this.resetNow = true;
|
||||
|
||||
|
|
@ -269,14 +269,12 @@ Contribute at: https://github.com/goatcorp/Dalamud
|
|||
|
||||
if (this.thankYouFont != null)
|
||||
{
|
||||
ImGui.PushFont(this.thankYouFont.ImFont);
|
||||
using var fontPush = this.thankYouFont.Push();
|
||||
var thankYouLenX = ImGui.CalcTextSize(ThankYouText).X;
|
||||
|
||||
ImGui.Dummy(new Vector2((windowX / 2) - (thankYouLenX / 2), 0f));
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted(ThankYouText);
|
||||
|
||||
ImGui.PopFont();
|
||||
}
|
||||
|
||||
ImGuiHelpers.ScaledDummy(0, windowSize.Y + 50f);
|
||||
|
|
@ -305,9 +303,5 @@ Contribute at: https://github.com/goatcorp/Dalamud
|
|||
/// <summary>
|
||||
/// Disposes of managed and unmanaged resources.
|
||||
/// </summary>
|
||||
public override void Dispose()
|
||||
{
|
||||
this.logoTexture?.Dispose();
|
||||
this.thankYouFont?.Dispose();
|
||||
}
|
||||
public override void Dispose() => this.privateAtlas.Dispose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
using CheapLoc;
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Interface.Colors;
|
||||
using Dalamud.Interface.Internal.Windows.PluginInstaller;
|
||||
using Dalamud.Interface.Internal.Windows.Settings.Widgets;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
using ImGuiNET;
|
||||
|
|
@ -28,7 +30,6 @@ public class SettingsTabLook : SettingsTab
|
|||
};
|
||||
|
||||
private float globalUiScale;
|
||||
private float fontGamma;
|
||||
|
||||
public override SettingsEntry[] Entries { get; } =
|
||||
{
|
||||
|
|
@ -41,9 +42,8 @@ public class SettingsTabLook : SettingsTab
|
|||
(v, c) => c.UseAxisFontsFromGame = v,
|
||||
v =>
|
||||
{
|
||||
var im = Service<InterfaceManager>.Get();
|
||||
im.UseAxisOverride = v;
|
||||
im.RebuildFonts();
|
||||
Service<FontAtlasFactory>.Get().UseAxisOverride = v;
|
||||
Service<InterfaceManager>.Get().RebuildFonts();
|
||||
}),
|
||||
|
||||
new GapSettingsEntry(5, true),
|
||||
|
|
@ -145,6 +145,7 @@ public class SettingsTabLook : SettingsTab
|
|||
public override void Draw()
|
||||
{
|
||||
var interfaceManager = Service<InterfaceManager>.Get();
|
||||
var fontBuildTask = interfaceManager.FontBuildTask;
|
||||
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global Font Scale"));
|
||||
|
|
@ -164,6 +165,19 @@ public class SettingsTabLook : SettingsTab
|
|||
}
|
||||
}
|
||||
|
||||
if (!fontBuildTask.IsCompleted)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
var buildingFonts = Loc.Localize("DalamudSettingsFontBuildInProgressWithEndingThreeDots", "Building fonts...");
|
||||
unsafe
|
||||
{
|
||||
var len = Encoding.UTF8.GetByteCount(buildingFonts);
|
||||
var p = stackalloc byte[len];
|
||||
Encoding.UTF8.GetBytes(buildingFonts, new(p, len));
|
||||
ImGuiNative.igTextUnformatted(p, (p + len + ((Environment.TickCount / 200) % 3)) - 2);
|
||||
}
|
||||
}
|
||||
|
||||
var globalUiScaleInPt = 12f * this.globalUiScale;
|
||||
if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp))
|
||||
{
|
||||
|
|
@ -174,33 +188,25 @@ public class SettingsTabLook : SettingsTab
|
|||
|
||||
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale text in all XIVLauncher UI elements - this is useful for 4K displays."));
|
||||
|
||||
ImGuiHelpers.ScaledDummy(5);
|
||||
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.Text(Loc.Localize("DalamudSettingsFontGamma", "Font Gamma"));
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsFontGammaReset"))
|
||||
if (fontBuildTask.IsFaulted || fontBuildTask.IsCanceled)
|
||||
{
|
||||
this.fontGamma = 1.4f;
|
||||
interfaceManager.FontGammaOverride = this.fontGamma;
|
||||
interfaceManager.RebuildFonts();
|
||||
ImGui.TextColored(
|
||||
ImGuiColors.DalamudRed,
|
||||
Loc.Localize("DalamudSettingsFontBuildFaulted", "Failed to load fonts as requested."));
|
||||
if (fontBuildTask.Exception is not null
|
||||
&& ImGui.CollapsingHeader("##DalamudSetingsFontBuildFaultReason"))
|
||||
{
|
||||
foreach (var e in fontBuildTask.Exception.InnerExceptions)
|
||||
ImGui.TextUnformatted(e.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, 0.3f, 3f, "%.2f", ImGuiSliderFlags.AlwaysClamp))
|
||||
{
|
||||
interfaceManager.FontGammaOverride = this.fontGamma;
|
||||
interfaceManager.RebuildFonts();
|
||||
}
|
||||
|
||||
ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsFontGammaHint", "Changes the thickness of text."));
|
||||
|
||||
base.Draw();
|
||||
}
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
this.globalUiScale = Service<DalamudConfiguration>.Get().GlobalUiScale;
|
||||
this.fontGamma = Service<DalamudConfiguration>.Get().FontGammaLevel;
|
||||
|
||||
base.Load();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,17 +31,20 @@ public sealed class LanguageChooserSettingsEntry : SettingsEntry
|
|||
try
|
||||
{
|
||||
var locLanguagesList = new List<string>();
|
||||
string locLanguage;
|
||||
foreach (var language in this.languages)
|
||||
{
|
||||
if (language != "ko")
|
||||
switch (language)
|
||||
{
|
||||
locLanguage = CultureInfo.GetCultureInfo(language).NativeName;
|
||||
locLanguagesList.Add(char.ToUpper(locLanguage[0]) + locLanguage[1..]);
|
||||
}
|
||||
else
|
||||
{
|
||||
locLanguagesList.Add("Korean");
|
||||
case "ko":
|
||||
locLanguagesList.Add("Korean");
|
||||
break;
|
||||
case "tw":
|
||||
locLanguagesList.Add("中華民國國語");
|
||||
break;
|
||||
default:
|
||||
string locLanguage = CultureInfo.GetCultureInfo(language).NativeName;
|
||||
locLanguagesList.Add(char.ToUpper(locLanguage[0]) + locLanguage[1..]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,11 +7,14 @@ using Dalamud.Game;
|
|||
using Dalamud.Game.ClientState;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Interface.Animation.EasingFunctions;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Storage.Assets;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
|
|
@ -27,16 +30,17 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
|
||||
private readonly ClientState clientState;
|
||||
private readonly DalamudConfiguration configuration;
|
||||
private readonly Framework framework;
|
||||
private readonly GameGui gameGui;
|
||||
private readonly TitleScreenMenu titleScreenMenu;
|
||||
|
||||
private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new();
|
||||
private readonly IFontAtlas privateAtlas;
|
||||
private readonly Lazy<IFontHandle> myFontHandle;
|
||||
private readonly Lazy<IDalamudTextureWrap> shadeTexture;
|
||||
|
||||
private readonly Dictionary<Guid, InOutCubic> shadeEasings = new();
|
||||
private readonly Dictionary<Guid, InOutQuint> moveEasings = new();
|
||||
private readonly Dictionary<Guid, InOutCubic> logoEasings = new();
|
||||
private readonly Dictionary<string, InterfaceManager.SpecialGlyphRequest> specialGlyphRequests = new();
|
||||
|
||||
private InOutCubic? fadeOutEasing;
|
||||
|
||||
|
|
@ -48,6 +52,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
/// <param name="clientState">An instance of <see cref="ClientState"/>.</param>
|
||||
/// <param name="configuration">An instance of <see cref="DalamudConfiguration"/>.</param>
|
||||
/// <param name="dalamudAssetManager">An instance of <see cref="DalamudAssetManager"/>.</param>
|
||||
/// <param name="fontAtlasFactory">An instance of <see cref="FontAtlasFactory"/>.</param>
|
||||
/// <param name="framework">An instance of <see cref="Framework"/>.</param>
|
||||
/// <param name="titleScreenMenu">An instance of <see cref="TitleScreenMenu"/>.</param>
|
||||
/// <param name="gameGui">An instance of <see cref="gameGui"/>.</param>
|
||||
|
|
@ -55,6 +60,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
ClientState clientState,
|
||||
DalamudConfiguration configuration,
|
||||
DalamudAssetManager dalamudAssetManager,
|
||||
FontAtlasFactory fontAtlasFactory,
|
||||
Framework framework,
|
||||
GameGui gameGui,
|
||||
TitleScreenMenu titleScreenMenu)
|
||||
|
|
@ -65,7 +71,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
{
|
||||
this.clientState = clientState;
|
||||
this.configuration = configuration;
|
||||
this.framework = framework;
|
||||
this.gameGui = gameGui;
|
||||
this.titleScreenMenu = titleScreenMenu;
|
||||
|
||||
|
|
@ -77,9 +82,25 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
this.PositionCondition = ImGuiCond.Always;
|
||||
this.RespectCloseHotkey = false;
|
||||
|
||||
this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade));
|
||||
this.privateAtlas = fontAtlasFactory.CreateFontAtlas(this.WindowName, FontAtlasAutoRebuildMode.Async);
|
||||
this.scopedFinalizer.Add(this.privateAtlas);
|
||||
|
||||
this.myFontHandle = new(
|
||||
() => this.scopedFinalizer.Add(
|
||||
this.privateAtlas.NewDelegateFontHandle(
|
||||
e => e.OnPreBuild(
|
||||
toolkit => toolkit.AddDalamudDefaultFont(
|
||||
TargetFontSizePx,
|
||||
titleScreenMenu.Entries.SelectMany(x => x.Name).ToGlyphRange())))));
|
||||
|
||||
titleScreenMenu.EntryListChange += this.TitleScreenMenuEntryListChange;
|
||||
this.scopedFinalizer.Add(() => titleScreenMenu.EntryListChange -= this.TitleScreenMenuEntryListChange);
|
||||
|
||||
this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade));
|
||||
|
||||
framework.Update += this.FrameworkOnUpdate;
|
||||
this.scopedFinalizer.Add(() => framework.Update -= this.FrameworkOnUpdate);
|
||||
}
|
||||
|
||||
private enum State
|
||||
|
|
@ -94,6 +115,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
/// </summary>
|
||||
public bool AllowDrawing { get; set; } = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose() => this.scopedFinalizer.Dispose();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void PreDraw()
|
||||
{
|
||||
|
|
@ -109,12 +133,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
base.PostDraw();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.framework.Update -= this.FrameworkOnUpdate;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Draw()
|
||||
{
|
||||
|
|
@ -246,33 +264,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var srcText = entries.Select(e => e.Name).ToHashSet();
|
||||
var keys = this.specialGlyphRequests.Keys.ToHashSet();
|
||||
keys.RemoveWhere(x => srcText.Contains(x));
|
||||
foreach (var key in keys)
|
||||
{
|
||||
this.specialGlyphRequests[key].Dispose();
|
||||
this.specialGlyphRequests.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
private bool DrawEntry(
|
||||
TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha, bool interactable)
|
||||
{
|
||||
InterfaceManager.SpecialGlyphRequest fontHandle;
|
||||
if (this.specialGlyphRequests.TryGetValue(entry.Name, out fontHandle) && fontHandle.Size != TargetFontSizePx)
|
||||
{
|
||||
fontHandle.Dispose();
|
||||
this.specialGlyphRequests.Remove(entry.Name);
|
||||
fontHandle = null;
|
||||
}
|
||||
|
||||
if (fontHandle == null)
|
||||
this.specialGlyphRequests[entry.Name] = fontHandle = Service<InterfaceManager>.Get().NewFontSizeRef(TargetFontSizePx, entry.Name);
|
||||
|
||||
ImGui.PushFont(fontHandle.Font);
|
||||
ImGui.SetWindowFontScale(TargetFontSizePx / fontHandle.Size);
|
||||
using var fontScopeDispose = this.myFontHandle.Value.Push();
|
||||
|
||||
var scale = ImGui.GetIO().FontGlobalScale;
|
||||
|
||||
|
|
@ -383,8 +380,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
initialCursor.Y += entry.Texture.Height * scale;
|
||||
ImGui.SetCursorPos(initialCursor);
|
||||
|
||||
ImGui.PopFont();
|
||||
|
||||
return isHover;
|
||||
}
|
||||
|
||||
|
|
@ -401,4 +396,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable
|
|||
if (charaMake != IntPtr.Zero || charaSelect != IntPtr.Zero || titleDcWorldMap != IntPtr.Zero)
|
||||
this.IsOpen = false;
|
||||
}
|
||||
|
||||
private void TitleScreenMenuEntryListChange() => this.privateAtlas.BuildFontsAsync();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// How to rebuild <see cref="IFontAtlas"/>.
|
||||
/// </summary>
|
||||
public enum FontAtlasAutoRebuildMode
|
||||
{
|
||||
/// <summary>
|
||||
/// Do not rebuild.
|
||||
/// </summary>
|
||||
Disable,
|
||||
|
||||
/// <summary>
|
||||
/// Rebuild on new frame.
|
||||
/// </summary>
|
||||
OnNewFrame,
|
||||
|
||||
/// <summary>
|
||||
/// Rebuild asynchronously.
|
||||
/// </summary>
|
||||
Async,
|
||||
}
|
||||
30
Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs
Normal file
30
Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Build step for <see cref="IFontAtlas"/>.
|
||||
/// </summary>
|
||||
public enum FontAtlasBuildStep
|
||||
{
|
||||
// Note: leave 0 alone; make default(FontAtlasBuildStep) not have a valid value
|
||||
|
||||
/// <summary>
|
||||
/// Called before calling <see cref="ImFontAtlasPtr.Build"/>.<br />
|
||||
/// Expect <see cref="IFontAtlasBuildToolkitPreBuild"/> to be passed.<br />
|
||||
/// When called from <see cref="IFontAtlas.BuildStepChange"/>, this will be called <b>before</b> the delegates
|
||||
/// passed to <see cref="IFontAtlas.NewDelegateFontHandle"/>.
|
||||
/// </summary>
|
||||
PreBuild = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Called after calling <see cref="ImFontAtlasPtr.Build"/>.<br />
|
||||
/// Expect <see cref="IFontAtlasBuildToolkitPostBuild"/> to be passed.<br />
|
||||
/// When called from <see cref="IFontAtlas.BuildStepChange"/>, this will be called <b>after</b> the delegates
|
||||
/// passed to <see cref="IFontAtlas.NewDelegateFontHandle"/>; you can do cross-font operations here.<br />
|
||||
/// <br />
|
||||
/// This callback is not guaranteed to happen after <see cref="PreBuild"/>,
|
||||
/// but it will never happen on its own.
|
||||
/// </summary>
|
||||
PostBuild = 2,
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate to be called when a font needs to be built.
|
||||
/// </summary>
|
||||
/// <param name="toolkit">A toolkit that may help you for font building steps.</param>
|
||||
/// <remarks>
|
||||
/// An implementation of <see cref="IFontAtlasBuildToolkit"/> may implement all of
|
||||
/// <see cref="IFontAtlasBuildToolkitPreBuild"/> and <see cref="IFontAtlasBuildToolkitPostBuild"/>.<br />
|
||||
/// Either use <see cref="IFontAtlasBuildToolkit.BuildStep"/> to identify the build step, or use
|
||||
/// <see cref="FontAtlasBuildToolkitUtilities.OnPreBuild"/> and <see cref="FontAtlasBuildToolkitUtilities.OnPostBuild"/>
|
||||
/// for routing.
|
||||
/// </remarks>
|
||||
public delegate void FontAtlasBuildStepDelegate(IFontAtlasBuildToolkit toolkit);
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using Dalamud.Interface.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Convenience function for building fonts through <see cref="IFontAtlas"/>.
|
||||
/// </summary>
|
||||
public static class FontAtlasBuildToolkitUtilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Compiles given <see cref="char"/>s into an array of <see cref="ushort"/> containing ImGui glyph ranges.
|
||||
/// </summary>
|
||||
/// <param name="enumerable">The chars.</param>
|
||||
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
|
||||
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
|
||||
/// <returns>The compiled range.</returns>
|
||||
public static ushort[] ToGlyphRange(
|
||||
this IEnumerable<char> enumerable,
|
||||
bool addFallbackCodepoints = true,
|
||||
bool addEllipsisCodepoints = true)
|
||||
{
|
||||
using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder);
|
||||
foreach (var c in enumerable)
|
||||
builder.AddChar(c);
|
||||
return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compiles given <see cref="char"/>s into an array of <see cref="ushort"/> containing ImGui glyph ranges.
|
||||
/// </summary>
|
||||
/// <param name="span">The chars.</param>
|
||||
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
|
||||
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
|
||||
/// <returns>The compiled range.</returns>
|
||||
public static ushort[] ToGlyphRange(
|
||||
this ReadOnlySpan<char> span,
|
||||
bool addFallbackCodepoints = true,
|
||||
bool addEllipsisCodepoints = true)
|
||||
{
|
||||
using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder);
|
||||
foreach (var c in span)
|
||||
builder.AddChar(c);
|
||||
return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compiles given string into an array of <see cref="ushort"/> containing ImGui glyph ranges.
|
||||
/// </summary>
|
||||
/// <param name="string">The string.</param>
|
||||
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
|
||||
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
|
||||
/// <returns>The compiled range.</returns>
|
||||
public static ushort[] ToGlyphRange(
|
||||
this string @string,
|
||||
bool addFallbackCodepoints = true,
|
||||
bool addEllipsisCodepoints = true) =>
|
||||
@string.AsSpan().ToGlyphRange(addFallbackCodepoints, addEllipsisCodepoints);
|
||||
|
||||
/// <summary>
|
||||
/// Finds the corresponding <see cref="ImFontConfigPtr"/> in
|
||||
/// <see cref="IFontAtlasBuildToolkit.NewImAtlas"/>.<see cref="ImFontAtlasPtr.ConfigData"/> that corresponds to the
|
||||
/// specified font <paramref name="fontPtr"/>.
|
||||
/// </summary>
|
||||
/// <param name="toolkit">The toolkit.</param>
|
||||
/// <param name="fontPtr">The font.</param>
|
||||
/// <returns>The relevant config pointer, or empty config pointer if not found.</returns>
|
||||
public static unsafe ImFontConfigPtr FindConfigPtr(this IFontAtlasBuildToolkit toolkit, ImFontPtr fontPtr)
|
||||
{
|
||||
foreach (ref var c in toolkit.NewImAtlas.ConfigDataWrapped().DataSpan)
|
||||
{
|
||||
if (c.DstFont == fontPtr.NativePtr)
|
||||
return new((nint)Unsafe.AsPointer(ref c));
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes <paramref name="action"/>
|
||||
/// if <see cref="IFontAtlasBuildToolkit.BuildStep"/> of <paramref name="toolkit"/>
|
||||
/// is <see cref="FontAtlasBuildStep.PreBuild"/>.
|
||||
/// </summary>
|
||||
/// <param name="toolkit">The toolkit.</param>
|
||||
/// <param name="action">The action.</param>
|
||||
/// <returns>This, for method chaining.</returns>
|
||||
public static IFontAtlasBuildToolkit OnPreBuild(
|
||||
this IFontAtlasBuildToolkit toolkit,
|
||||
Action<IFontAtlasBuildToolkitPreBuild> action)
|
||||
{
|
||||
if (toolkit.BuildStep is FontAtlasBuildStep.PreBuild)
|
||||
action.Invoke((IFontAtlasBuildToolkitPreBuild)toolkit);
|
||||
return toolkit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes <paramref name="action"/>
|
||||
/// if <see cref="IFontAtlasBuildToolkit.BuildStep"/> of <paramref name="toolkit"/>
|
||||
/// is <see cref="FontAtlasBuildStep.PostBuild"/>.
|
||||
/// </summary>
|
||||
/// <param name="toolkit">The toolkit.</param>
|
||||
/// <param name="action">The action.</param>
|
||||
/// <returns>toolkit, for method chaining.</returns>
|
||||
public static IFontAtlasBuildToolkit OnPostBuild(
|
||||
this IFontAtlasBuildToolkit toolkit,
|
||||
Action<IFontAtlasBuildToolkitPostBuild> action)
|
||||
{
|
||||
if (toolkit.BuildStep is FontAtlasBuildStep.PostBuild)
|
||||
action.Invoke((IFontAtlasBuildToolkitPostBuild)toolkit);
|
||||
return toolkit;
|
||||
}
|
||||
}
|
||||
143
Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs
Normal file
143
Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for <see cref="ImFontAtlasPtr"/>.
|
||||
/// </summary>
|
||||
public interface IFontAtlas : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Event to be called on build step changes.<br />
|
||||
/// <see cref="IFontAtlasBuildToolkit.Font"/> is meaningless for this event.
|
||||
/// </summary>
|
||||
event FontAtlasBuildStepDelegate? BuildStepChange;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when a font rebuild operation is recommended.<br />
|
||||
/// This event will be invoked from the main thread.<br />
|
||||
/// <br />
|
||||
/// Reasons for the event include changes in <see cref="ImGuiHelpers.GlobalScale"/> and
|
||||
/// initialization of new associated font handles.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// You should call <see cref="BuildFontsAsync"/> or <see cref="BuildFontsOnNextFrame"/>
|
||||
/// if <see cref="AutoRebuildMode"/> is not set to <c>true</c>.<br />
|
||||
/// Avoid calling <see cref="BuildFontsImmediately"/> here; it will block the main thread.
|
||||
/// </remarks>
|
||||
event Action? RebuildRecommend;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the atlas. For logging and debugging purposes.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value how the atlas should be rebuilt when the relevant Dalamud Configuration changes.
|
||||
/// </summary>
|
||||
FontAtlasAutoRebuildMode AutoRebuildMode { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font atlas. Might be empty.
|
||||
/// </summary>
|
||||
ImFontAtlasPtr ImAtlas { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the task that represents the current font rebuild state.
|
||||
/// </summary>
|
||||
Task BuildTask { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether there exists any built atlas, regardless of <see cref="BuildTask"/>.
|
||||
/// </summary>
|
||||
bool HasBuiltAtlas { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this font atlas is under the effect of global scale.
|
||||
/// </summary>
|
||||
bool IsGlobalScaled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Suppresses automatically rebuilding fonts for the scope.
|
||||
/// </summary>
|
||||
/// <returns>An instance of <see cref="IDisposable"/> that will release the suppression.</returns>
|
||||
/// <remarks>
|
||||
/// Use when you will be creating multiple new handles, and want rebuild to trigger only when you're done doing so.
|
||||
/// This function will effectively do nothing, if <see cref="AutoRebuildMode"/> is set to
|
||||
/// <see cref="FontAtlasAutoRebuildMode.Disable"/>.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// using (atlas.SuppressBuild()) {
|
||||
/// this.font1 = atlas.NewGameFontHandle(...);
|
||||
/// this.font2 = atlas.NewDelegateFontHandle(...);
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public IDisposable SuppressAutoRebuild();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="IFontHandle"/> from game's built-in fonts.
|
||||
/// </summary>
|
||||
/// <param name="style">Font to use.</param>
|
||||
/// <returns>Handle to a font that may or may not be ready yet.</returns>
|
||||
public IFontHandle NewGameFontHandle(GameFontStyle style);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new IFontHandle using your own callbacks.
|
||||
/// </summary>
|
||||
/// <param name="buildStepDelegate">Callback for <see cref="IFontAtlas.BuildStepChange"/>.</param>
|
||||
/// <returns>Handle to a font that may or may not be ready yet.</returns>
|
||||
/// <example>
|
||||
/// <b>On initialization</b>:
|
||||
/// <code>
|
||||
/// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => {
|
||||
/// var config = new SafeFontConfig { SizePx = 16 };
|
||||
/// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config);
|
||||
/// tk.AddGameSymbol(config);
|
||||
/// tk.AddExtraGlyphsForDalamudLanguage(config);
|
||||
/// // optionally do the following if you have to add more than one font here,
|
||||
/// // to specify which font added during this delegate is the final font to use.
|
||||
/// tk.Font = config.MergeFont;
|
||||
/// }));
|
||||
/// // or
|
||||
/// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(36)));
|
||||
/// </code>
|
||||
/// <br />
|
||||
/// <b>On use</b>:
|
||||
/// <code>
|
||||
/// using (this.fontHandle.Push())
|
||||
/// ImGui.TextUnformatted("Example");
|
||||
/// </code>
|
||||
/// </example>
|
||||
public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate);
|
||||
|
||||
/// <summary>
|
||||
/// Queues rebuilding fonts, on the main thread.<br />
|
||||
/// Note that <see cref="BuildTask"/> would not necessarily get changed from calling this function.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">If <see cref="AutoRebuildMode"/> is <see cref="FontAtlasAutoRebuildMode.Async"/>.</exception>
|
||||
/// <remarks>
|
||||
/// Using this method will block the main thread on rebuilding fonts, effectively calling
|
||||
/// <see cref="BuildFontsImmediately"/> from the main thread. Consider migrating to <see cref="BuildFontsAsync"/>.
|
||||
/// </remarks>
|
||||
void BuildFontsOnNextFrame();
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds fonts immediately, on the current thread.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">If <see cref="AutoRebuildMode"/> is <see cref="FontAtlasAutoRebuildMode.Async"/>.</exception>
|
||||
void BuildFontsImmediately();
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds fonts asynchronously, on any thread.
|
||||
/// </summary>
|
||||
/// <returns>The task.</returns>
|
||||
/// <exception cref="InvalidOperationException">If <see cref="AutoRebuildMode"/> is <see cref="FontAtlasAutoRebuildMode.OnNewFrame"/>.</exception>
|
||||
Task BuildFontsAsync();
|
||||
}
|
||||
91
Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs
Normal file
91
Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Common stuff for <see cref="IFontAtlasBuildToolkitPreBuild"/> and <see cref="IFontAtlasBuildToolkitPostBuild"/>.
|
||||
/// </summary>
|
||||
public interface IFontAtlasBuildToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// Functionalities for compatibility behavior.<br />
|
||||
/// </summary>
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
internal interface IApi9Compat : IFontAtlasBuildToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes <paramref name="action"/>, temporarily applying <see cref="IFontHandleSubstance"/>s.<br />
|
||||
/// </summary>
|
||||
/// <param name="action">The action to invoke.</param>
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public void FromUiBuilderObsoleteEventHandlers(Action action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the font relevant to the call.
|
||||
/// </summary>
|
||||
ImFontPtr Font { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current scale this font atlas is being built with.
|
||||
/// </summary>
|
||||
float Scale { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the current build operation is asynchronous.
|
||||
/// </summary>
|
||||
bool IsAsyncBuildOperation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current build step.
|
||||
/// </summary>
|
||||
FontAtlasBuildStep BuildStep { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font atlas being built.
|
||||
/// </summary>
|
||||
ImFontAtlasPtr NewImAtlas { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the wrapper for <see cref="ImFontAtlas.Fonts"/> of <see cref="NewImAtlas"/>.<br />
|
||||
/// This does not need to be disposed. Calling <see cref="IDisposable.Dispose"/> does nothing.-
|
||||
/// <br />
|
||||
/// Modification of this vector may result in undefined behaviors.
|
||||
/// </summary>
|
||||
ImVectorWrapper<ImFontPtr> Fonts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Queues an item to be disposed after the native atlas gets disposed, successful or not.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Disposable type.</typeparam>
|
||||
/// <param name="disposable">The disposable.</param>
|
||||
/// <returns>The same <paramref name="disposable"/>.</returns>
|
||||
T DisposeWithAtlas<T>(T disposable) where T : IDisposable;
|
||||
|
||||
/// <summary>
|
||||
/// Queues an item to be disposed after the native atlas gets disposed, successful or not.
|
||||
/// </summary>
|
||||
/// <param name="gcHandle">The gc handle.</param>
|
||||
/// <returns>The same <paramref name="gcHandle"/>.</returns>
|
||||
GCHandle DisposeWithAtlas(GCHandle gcHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Queues an item to be disposed after the native atlas gets disposed, successful or not.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to run on dispose.</param>
|
||||
void DisposeWithAtlas(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the instance of <see cref="ImFontPtr"/> corresponding to <paramref name="fontHandle"/>
|
||||
/// from <see cref="NewImAtlas"/>.
|
||||
/// </summary>
|
||||
/// <param name="fontHandle">The font handle.</param>
|
||||
/// <returns>The corresonding <see cref="ImFontPtr"/>, or default if not found.</returns>
|
||||
ImFontPtr GetFont(IFontHandle fontHandle);
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
using Dalamud.Interface.Internal;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PostBuild"/>.
|
||||
/// </summary>
|
||||
public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether global scaling is ignored for the given font.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">The font.</param>
|
||||
/// <returns>True if ignored.</returns>
|
||||
bool IsGlobalScaleIgnored(ImFontPtr fontPtr);
|
||||
|
||||
/// <summary>
|
||||
/// Stores a texture to be managed with the atlas.
|
||||
/// </summary>
|
||||
/// <param name="textureWrap">The texture wrap.</param>
|
||||
/// <param name="disposeOnError">Dispose the wrap on error.</param>
|
||||
/// <returns>The texture index.</returns>
|
||||
int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError);
|
||||
|
||||
/// <summary>
|
||||
/// Copies glyphs across fonts, in a safer way.<br />
|
||||
/// If the font does not belong to the current atlas, this function is a no-op.
|
||||
/// </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>
|
||||
/// <param name="rangeLow">Low codepoint range to copy.</param>
|
||||
/// <param name="rangeHigh">High codepoing range to copy.</param>
|
||||
void CopyGlyphsAcrossFonts(
|
||||
ImFontPtr source,
|
||||
ImFontPtr target,
|
||||
bool missingOnly,
|
||||
bool rebuildLookupTable = true,
|
||||
char rangeLow = ' ',
|
||||
char rangeHigh = '\uFFFE');
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="ImFontPtr.BuildLookupTable"/>, with some fixups.
|
||||
/// </summary>
|
||||
/// <param name="font">The font.</param>
|
||||
void BuildLookupTable(ImFontPtr font);
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Toolkit for use when the build state is <see cref="FontAtlasBuildStep.PreBuild"/>.<br />
|
||||
/// <br />
|
||||
/// After <see cref="FontAtlasBuildStepDelegate"/> returns,
|
||||
/// either <see cref="IFontAtlasBuildToolkit.Font"/> must be set,
|
||||
/// or at least one font must have been added to the atlas using one of AddFont... functions.
|
||||
/// </summary>
|
||||
public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit
|
||||
{
|
||||
/// <summary>
|
||||
/// Queues an item to be disposed after the whole build process gets complete, successful or not.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Disposable type.</typeparam>
|
||||
/// <param name="disposable">The disposable.</param>
|
||||
/// <returns>The same <paramref name="disposable"/>.</returns>
|
||||
T DisposeAfterBuild<T>(T disposable) where T : IDisposable;
|
||||
|
||||
/// <summary>
|
||||
/// Queues an item to be disposed after the whole build process gets complete, successful or not.
|
||||
/// </summary>
|
||||
/// <param name="gcHandle">The gc handle.</param>
|
||||
/// <returns>The same <paramref name="gcHandle"/>.</returns>
|
||||
GCHandle DisposeAfterBuild(GCHandle gcHandle);
|
||||
|
||||
/// <summary>
|
||||
/// Queues an item to be disposed after the whole build process gets complete, successful or not.
|
||||
/// </summary>
|
||||
/// <param name="action">The action to run on dispose.</param>
|
||||
void DisposeAfterBuild(Action action);
|
||||
|
||||
/// <summary>
|
||||
/// Excludes given font from global scaling.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">The font.</param>
|
||||
/// <returns>Same <see cref="ImFontPtr"/> with <paramref name="fontPtr"/>.</returns>
|
||||
ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether global scaling is ignored for the given font.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">The font.</param>
|
||||
/// <returns>True if ignored.</returns>
|
||||
bool IsGlobalScaleIgnored(ImFontPtr fontPtr);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a font from memory region allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.<br />
|
||||
/// <b>It WILL crash if you try to use a memory pointer allocated in some other way.</b><br />
|
||||
/// <b>
|
||||
/// Do NOT call <see cref="ImGuiNative.igMemFree"/> on the <paramref name="dataPointer"/> once this function has
|
||||
/// been called, unless <paramref name="freeOnException"/> is set and the function has thrown an error.
|
||||
/// </b>
|
||||
/// </summary>
|
||||
/// <param name="dataPointer">Memory address for the data allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.</param>
|
||||
/// <param name="dataSize">The size of the font file..</param>
|
||||
/// <param name="fontConfig">The font config.</param>
|
||||
/// <param name="freeOnException">Free <paramref name="dataPointer"/> if an exception happens.</param>
|
||||
/// <param name="debugTag">A debug tag.</param>
|
||||
/// <returns>The newly added font.</returns>
|
||||
unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory(
|
||||
nint dataPointer,
|
||||
int dataSize,
|
||||
in SafeFontConfig fontConfig,
|
||||
bool freeOnException,
|
||||
string debugTag)
|
||||
=> this.AddFontFromImGuiHeapAllocatedMemory(
|
||||
(void*)dataPointer,
|
||||
dataSize,
|
||||
fontConfig,
|
||||
freeOnException,
|
||||
debugTag);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a font from memory region allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.<br />
|
||||
/// <b>It WILL crash if you try to use a memory pointer allocated in some other way.</b><br />
|
||||
/// <b>
|
||||
/// Do NOT call <see cref="ImGuiNative.igMemFree"/> on the <paramref name="dataPointer"/> once this function has
|
||||
/// been called, unless <paramref name="freeOnException"/> is set and the function has thrown an error.
|
||||
/// </b>
|
||||
/// </summary>
|
||||
/// <param name="dataPointer">Memory address for the data allocated using <see cref="ImGuiHelpers.AllocateMemory"/>.</param>
|
||||
/// <param name="dataSize">The size of the font file..</param>
|
||||
/// <param name="fontConfig">The font config.</param>
|
||||
/// <param name="freeOnException">Free <paramref name="dataPointer"/> if an exception happens.</param>
|
||||
/// <param name="debugTag">A debug tag.</param>
|
||||
/// <returns>The newly added font.</returns>
|
||||
unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory(
|
||||
void* dataPointer,
|
||||
int dataSize,
|
||||
in SafeFontConfig fontConfig,
|
||||
bool freeOnException,
|
||||
string debugTag);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a font from a file.
|
||||
/// </summary>
|
||||
/// <param name="path">The file path to create a new font from.</param>
|
||||
/// <param name="fontConfig">The font config.</param>
|
||||
/// <returns>The newly added font.</returns>
|
||||
ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a font from a stream.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to create a new font from.</param>
|
||||
/// <param name="fontConfig">The font config.</param>
|
||||
/// <param name="leaveOpen">Dispose when this function returns or throws.</param>
|
||||
/// <param name="debugTag">A debug tag.</param>
|
||||
/// <returns>The newly added font.</returns>
|
||||
ImFontPtr AddFontFromStream(Stream stream, in SafeFontConfig fontConfig, bool leaveOpen, string debugTag);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a font from memory.
|
||||
/// </summary>
|
||||
/// <param name="span">The span to create from.</param>
|
||||
/// <param name="fontConfig">The font config.</param>
|
||||
/// <param name="debugTag">A debug tag.</param>
|
||||
/// <returns>The newly added font.</returns>
|
||||
ImFontPtr AddFontFromMemory(ReadOnlySpan<byte> span, in SafeFontConfig fontConfig, string debugTag);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the default font known to the current font atlas.<br />
|
||||
/// <br />
|
||||
/// Includes <see cref="AddFontAwesomeIconFont"/> and <see cref="AttachExtraGlyphsForDalamudLanguage"/>.<br />
|
||||
/// As this involves adding multiple fonts, calling this function will set <see cref="IFontAtlasBuildToolkit.Font"/>
|
||||
/// as the return value of this function, if it was empty before.
|
||||
/// </summary>
|
||||
/// <param name="sizePx">Font size in pixels.</param>
|
||||
/// <param name="glyphRanges">The glyph ranges. Use <see cref="FontAtlasBuildToolkitUtilities"/>.ToGlyphRange to build.</param>
|
||||
/// <returns>A font returned from <see cref="ImFontAtlasPtr.AddFont"/>.</returns>
|
||||
ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a font that is shipped with Dalamud.<br />
|
||||
/// <br />
|
||||
/// Note: if game symbols font file is requested but is unavailable,
|
||||
/// then it will take the glyphs from game's built-in fonts, and everything in <paramref name="fontConfig"/>
|
||||
/// will be ignored but <see cref="SafeFontConfig.SizePx"/>, <see cref="SafeFontConfig.MergeFont"/>,
|
||||
/// and <see cref="SafeFontConfig.GlyphRanges"/>.
|
||||
/// </summary>
|
||||
/// <param name="asset">The font type.</param>
|
||||
/// <param name="fontConfig">The font config.</param>
|
||||
/// <returns>The added font.</returns>
|
||||
ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig);
|
||||
|
||||
/// <summary>
|
||||
/// Same with <see cref="AddDalamudAssetFont"/>(<see cref="DalamudAsset.FontAwesomeFreeSolid"/>, ...),
|
||||
/// but using only FontAwesome icon ranges.<br />
|
||||
/// <see cref="SafeFontConfig.GlyphRanges"/> will be ignored.
|
||||
/// </summary>
|
||||
/// <param name="fontConfig">The font config.</param>
|
||||
/// <returns>The added font.</returns>
|
||||
ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the game's symbols into the provided font.<br />
|
||||
/// <see cref="SafeFontConfig.GlyphRanges"/> will be ignored.<br />
|
||||
/// If the game symbol font file is unavailable, only <see cref="SafeFontConfig.SizePx"/> will be honored.
|
||||
/// </summary>
|
||||
/// <param name="fontConfig">The font config.</param>
|
||||
/// <returns>The added font.</returns>
|
||||
ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the game glyphs to the font.
|
||||
/// </summary>
|
||||
/// <param name="gameFontStyle">The font style.</param>
|
||||
/// <param name="glyphRanges">The glyph ranges.</param>
|
||||
/// <param name="mergeFont">The font to merge to. If empty, then a new font will be created.</param>
|
||||
/// <returns>The added font.</returns>
|
||||
ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont);
|
||||
|
||||
/// <summary>
|
||||
/// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.<br />
|
||||
/// <see cref="SafeFontConfig.GlyphRanges"/> will be ignored.
|
||||
/// </summary>
|
||||
/// <param name="fontConfig">The font config.</param>
|
||||
void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig);
|
||||
}
|
||||
76
Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs
Normal file
76
Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
using System.Threading.Tasks;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a reference counting handle for fonts.
|
||||
/// </summary>
|
||||
public interface IFontHandle : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Delegate for <see cref="IFontHandle.ImFontChanged"/>.
|
||||
/// </summary>
|
||||
/// <param name="fontHandle">The relevant font handle.</param>
|
||||
/// <param name="lockedFont">The locked font for this font handle, locked during the call of this delegate.</param>
|
||||
public delegate void ImFontChangedDelegate(IFontHandle fontHandle, ILockedImFont lockedFont);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the built instance of <see cref="ImFontPtr"/> has been changed.<br />
|
||||
/// This event can be invoked outside the main thread.
|
||||
/// </summary>
|
||||
event ImFontChangedDelegate ImFontChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the load exception, if it failed to load. Otherwise, it is null.
|
||||
/// </summary>
|
||||
Exception? LoadException { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this font is ready for use.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use <see cref="Push"/> directly if you want to keep the current ImGui font if the font is not ready.<br />
|
||||
/// Alternatively, use <see cref="WaitAsync"/> to wait for this property to become <c>true</c>.
|
||||
/// </remarks>
|
||||
bool Available { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Locks the fully constructed instance of <see cref="ImFontPtr"/> corresponding to the this
|
||||
/// <see cref="IFontHandle"/>, for use in any thread.<br />
|
||||
/// Modification of the font will exhibit undefined behavior if some other thread also uses the font.
|
||||
/// </summary>
|
||||
/// <returns>An instance of <see cref="ILockedImFont"/> that <b>must</b> be disposed after use.</returns>
|
||||
/// <remarks>
|
||||
/// Calling <see cref="IFontHandle"/>.<see cref="IDisposable.Dispose"/> will not unlock the <see cref="ImFontPtr"/>
|
||||
/// locked by this function.
|
||||
/// </remarks>
|
||||
/// <exception cref="InvalidOperationException">If <see cref="Available"/> is <c>false</c>.</exception>
|
||||
ILockedImFont Lock();
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the current font into ImGui font stack, if available.<br />
|
||||
/// Use <see cref="ImGui.GetFont"/> to access the current font.<br />
|
||||
/// You may not access the font once you dispose this object.
|
||||
/// </summary>
|
||||
/// <returns>A disposable object that will pop the font on dispose.</returns>
|
||||
/// <exception cref="InvalidOperationException">If called outside of the main thread.</exception>
|
||||
/// <remarks>
|
||||
/// This function uses <see cref="ImGui.PushFont"/>, and may do extra things.
|
||||
/// Use <see cref="IDisposable.Dispose"/> or <see cref="Pop"/> to undo this operation.
|
||||
/// Do not use <see cref="ImGui.PopFont"/>.
|
||||
/// </remarks>
|
||||
IDisposable Push();
|
||||
|
||||
/// <summary>
|
||||
/// Pops the font pushed to ImGui using <see cref="Push"/>, cleaning up any extra information as needed.
|
||||
/// </summary>
|
||||
void Pop();
|
||||
|
||||
/// <summary>
|
||||
/// Waits for <see cref="Available"/> to become <c>true</c>.
|
||||
/// </summary>
|
||||
/// <returns>A task containing this <see cref="IFontHandle"/>.</returns>
|
||||
Task<IFontHandle> WaitAsync();
|
||||
}
|
||||
21
Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs
Normal file
21
Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// The wrapper for <see cref="ImFontPtr"/>, guaranteeing that the associated data will be available as long as
|
||||
/// this struct is not disposed.
|
||||
/// </summary>
|
||||
public interface ILockedImFont : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the associated <see cref="ImFontPtr"/>.
|
||||
/// </summary>
|
||||
ImFontPtr ImFont { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ILockedImFont"/> with an additional reference to the owner.
|
||||
/// </summary>
|
||||
/// <returns>The new locked instance.</returns>
|
||||
ILockedImFont NewRef();
|
||||
}
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// A font handle representing a user-callback generated font.
|
||||
/// </summary>
|
||||
internal sealed class DelegateFontHandle : FontHandle
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DelegateFontHandle"/> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">An instance of <see cref="IFontHandleManager"/>.</param>
|
||||
/// <param name="callOnBuildStepChange">Callback for <see cref="IFontAtlas.BuildStepChange"/>.</param>
|
||||
public DelegateFontHandle(IFontHandleManager manager, FontAtlasBuildStepDelegate callOnBuildStepChange)
|
||||
: base(manager)
|
||||
{
|
||||
this.CallOnBuildStepChange = callOnBuildStepChange;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the function to be called on build step changes.
|
||||
/// </summary>
|
||||
public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Manager for <see cref="DelegateFontHandle"/>s.
|
||||
/// </summary>
|
||||
internal sealed class HandleManager : IFontHandleManager
|
||||
{
|
||||
private readonly HashSet<DelegateFontHandle> handles = new();
|
||||
private readonly object syncRoot = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HandleManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="atlasName">The name of the owner atlas.</param>
|
||||
public HandleManager(string atlasName) => this.Name = $"{atlasName}:{nameof(DelegateFontHandle)}:Manager";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action? RebuildRecommend;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFontHandleSubstance? Substance { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
lock (this.syncRoot)
|
||||
this.handles.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IFontAtlas.NewDelegateFontHandle"/>
|
||||
public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate buildStepDelegate)
|
||||
{
|
||||
var key = new DelegateFontHandle(this, buildStepDelegate);
|
||||
lock (this.syncRoot)
|
||||
this.handles.Add(key);
|
||||
this.RebuildRecommend?.Invoke();
|
||||
return key;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void FreeFontHandle(IFontHandle handle)
|
||||
{
|
||||
if (handle is not DelegateFontHandle cgfh)
|
||||
return;
|
||||
|
||||
lock (this.syncRoot)
|
||||
this.handles.Remove(cgfh);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFontHandleSubstance NewSubstance(IRefCountable dataRoot)
|
||||
{
|
||||
lock (this.syncRoot)
|
||||
return new HandleSubstance(this, dataRoot, this.handles.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Substance from <see cref="HandleManager"/>.
|
||||
/// </summary>
|
||||
internal sealed class HandleSubstance : IFontHandleSubstance
|
||||
{
|
||||
private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}");
|
||||
|
||||
// Owned by this class, but ImFontPtr values still do not belong to this.
|
||||
private readonly Dictionary<DelegateFontHandle, ImFontPtr> fonts = new();
|
||||
private readonly Dictionary<DelegateFontHandle, Exception?> buildExceptions = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HandleSubstance"/> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">The manager.</param>
|
||||
/// <param name="dataRoot">The data root.</param>
|
||||
/// <param name="relevantHandles">The relevant handles.</param>
|
||||
public HandleSubstance(
|
||||
IFontHandleManager manager,
|
||||
IRefCountable dataRoot,
|
||||
DelegateFontHandle[] relevantHandles)
|
||||
{
|
||||
// We do not call dataRoot.AddRef; this object is dependant on lifetime of dataRoot.
|
||||
|
||||
this.Manager = manager;
|
||||
this.DataRoot = dataRoot;
|
||||
this.RelevantHandles = relevantHandles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the relevant handles.
|
||||
/// </summary>
|
||||
// Not owned by this class. Do not dispose.
|
||||
public DelegateFontHandle[] RelevantHandles { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
ICollection<FontHandle> IFontHandleSubstance.RelevantHandles => this.RelevantHandles;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IRefCountable DataRoot { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFontHandleManager Manager { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public IFontAtlasBuildToolkitPreBuild? PreBuildToolkitForApi9Compat { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public bool CreateFontOnAccess { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.fonts.Clear();
|
||||
this.buildExceptions.Clear();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr GetFontPtr(IFontHandle handle) =>
|
||||
handle is DelegateFontHandle cgfh ? this.fonts.GetValueOrDefault(cgfh) : default;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Exception? GetBuildException(IFontHandle handle) =>
|
||||
handle is DelegateFontHandle cgfh ? this.buildExceptions.GetValueOrDefault(cgfh) : default;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
|
||||
{
|
||||
var fontsVector = toolkitPreBuild.Fonts;
|
||||
foreach (var k in this.RelevantHandles)
|
||||
{
|
||||
var fontCountPrevious = fontsVector.Length;
|
||||
|
||||
try
|
||||
{
|
||||
toolkitPreBuild.Font = default;
|
||||
k.CallOnBuildStepChange(toolkitPreBuild);
|
||||
if (toolkitPreBuild.Font.IsNull())
|
||||
{
|
||||
if (fontCountPrevious == fontsVector.Length)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(FontAtlasBuildStepDelegate)} must either set the " +
|
||||
$"{nameof(IFontAtlasBuildToolkitPreBuild.Font)} property, or add at least one font.");
|
||||
}
|
||||
|
||||
toolkitPreBuild.Font = fontsVector[^1];
|
||||
}
|
||||
else
|
||||
{
|
||||
var found = false;
|
||||
unsafe
|
||||
{
|
||||
for (var i = fontCountPrevious; !found && i < fontsVector.Length; i++)
|
||||
{
|
||||
if (fontsVector[i].NativePtr == toolkitPreBuild.Font.NativePtr)
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"The font does not exist in the atlas' font array. If you need an empty font, try" +
|
||||
"adding Noto Sans from Dalamud Assets, but using new ushort[]{ ' ', ' ', 0 } as the" +
|
||||
"glyph range.");
|
||||
}
|
||||
}
|
||||
|
||||
if (fontsVector.Length - fontCountPrevious != 1)
|
||||
{
|
||||
Log.Warning(
|
||||
"[{name}:Substance] {n} fonts added from {delegate} PreBuild call; " +
|
||||
"Using the most recently added font. " +
|
||||
"Did you mean to use {sfd}.{sfdprop} or {ifcp}.{ifcpprop}?",
|
||||
this.Manager.Name,
|
||||
fontsVector.Length - fontCountPrevious,
|
||||
nameof(FontAtlasBuildStepDelegate),
|
||||
nameof(SafeFontConfig),
|
||||
nameof(SafeFontConfig.MergeFont),
|
||||
nameof(ImFontConfigPtr),
|
||||
nameof(ImFontConfigPtr.MergeMode));
|
||||
}
|
||||
|
||||
for (var i = fontCountPrevious; i < fontsVector.Length; i++)
|
||||
{
|
||||
if (fontsVector[i].ValidateUnsafe() is { } ex)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"One of the newly added fonts seem to be pointing to an invalid memory address.",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicate entries; duplicates will result in free-after-free
|
||||
for (var i = 0; i < fontCountPrevious; i++)
|
||||
{
|
||||
for (var j = fontCountPrevious; j < fontsVector.Length; j++)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
if (fontsVector[i].NativePtr == fontsVector[j].NativePtr)
|
||||
throw new InvalidOperationException("An already added font has been added again.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.fonts[k] = toolkitPreBuild.Font;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.fonts[k] = default;
|
||||
this.buildExceptions[k] = e;
|
||||
|
||||
Log.Error(
|
||||
e,
|
||||
"[{name}:Substance] An error has occurred while during {delegate} PreBuild call.",
|
||||
this.Manager.Name,
|
||||
nameof(FontAtlasBuildStepDelegate));
|
||||
|
||||
// Sanitization, in a futile attempt to prevent crashes on invalid parameters
|
||||
unsafe
|
||||
{
|
||||
var distinct =
|
||||
fontsVector
|
||||
.DistinctBy(x => (nint)x.NativePtr) // Remove duplicates
|
||||
.Where(x => x.ValidateUnsafe() is null) // Remove invalid entries without freeing them
|
||||
.ToArray();
|
||||
|
||||
// We're adding the contents back; do not destroy the contents
|
||||
fontsVector.Clear(true);
|
||||
fontsVector.AddRange(distinct.AsSpan());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
|
||||
{
|
||||
// irrelevant
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
|
||||
{
|
||||
foreach (var k in this.RelevantHandles)
|
||||
{
|
||||
if (!this.fonts[k].IsNotNullAndLoaded())
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
toolkitPostBuild.Font = this.fonts[k];
|
||||
k.CallOnBuildStepChange.Invoke(toolkitPostBuild);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.fonts[k] = default;
|
||||
this.buildExceptions[k] = e;
|
||||
|
||||
Log.Error(
|
||||
e,
|
||||
"[{name}] An error has occurred while during {delegate} PostBuild call.",
|
||||
this.Manager.Name,
|
||||
nameof(FontAtlasBuildStepDelegate));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,680 @@
|
|||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Unicode;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Storage.Assets;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using SharpDX.DXGI;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Standalone font atlas.
|
||||
/// </summary>
|
||||
internal sealed partial class FontAtlasFactory
|
||||
{
|
||||
private static readonly Dictionary<ulong, List<(char Left, char Right, float Distance)>> PairAdjustmentsCache =
|
||||
new();
|
||||
|
||||
/// <summary>
|
||||
/// Implementations for <see cref="IFontAtlasBuildToolkitPreBuild"/> and
|
||||
/// <see cref="IFontAtlasBuildToolkitPostBuild"/>.
|
||||
/// </summary>
|
||||
private class BuildToolkit : IFontAtlasBuildToolkit.IApi9Compat, IFontAtlasBuildToolkitPreBuild, IFontAtlasBuildToolkitPostBuild, IDisposable
|
||||
{
|
||||
private static readonly ushort FontAwesomeIconMin =
|
||||
(ushort)Enum.GetValues<FontAwesomeIcon>().Where(x => x > 0).Min();
|
||||
|
||||
private static readonly ushort FontAwesomeIconMax =
|
||||
(ushort)Enum.GetValues<FontAwesomeIcon>().Where(x => x > 0).Max();
|
||||
|
||||
private readonly DisposeSafety.ScopedFinalizer disposeAfterBuild = new();
|
||||
private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance;
|
||||
private readonly FontAtlasFactory factory;
|
||||
private readonly FontAtlasBuiltData data;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BuildToolkit"/> class.
|
||||
/// </summary>
|
||||
/// <param name="factory">An instance of <see cref="FontAtlasFactory"/>.</param>
|
||||
/// <param name="data">New atlas.</param>
|
||||
/// <param name="gameFontHandleSubstance">An instance of <see cref="GamePrebakedFontHandle.HandleSubstance"/>.</param>
|
||||
/// <param name="isAsync">Specify whether the current build operation is an asynchronous one.</param>
|
||||
public BuildToolkit(
|
||||
FontAtlasFactory factory,
|
||||
FontAtlasBuiltData data,
|
||||
GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance,
|
||||
bool isAsync)
|
||||
{
|
||||
this.data = data;
|
||||
this.gameFontHandleSubstance = gameFontHandleSubstance;
|
||||
this.IsAsyncBuildOperation = isAsync;
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IFontAtlasBuildToolkit.Font"/>
|
||||
public ImFontPtr Font { get; set; }
|
||||
|
||||
/// <inheritdoc cref="IFontAtlasBuildToolkit.Font"/>
|
||||
public float Scale => this.data.Scale;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsAsyncBuildOperation { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FontAtlasBuildStep BuildStep { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontAtlasPtr NewImAtlas => this.data.Atlas;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImVectorWrapper<ImFontPtr> Fonts => this.data.Fonts;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of fonts to ignore global scale.
|
||||
/// </summary>
|
||||
public List<ImFontPtr> GlobalScaleExclusions { get; } = new();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose() => this.disposeAfterBuild.Dispose();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T2 DisposeAfterBuild<T2>(T2 disposable) where T2 : IDisposable =>
|
||||
this.disposeAfterBuild.Add(disposable);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GCHandle DisposeAfterBuild(GCHandle gcHandle) => this.disposeAfterBuild.Add(gcHandle);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DisposeAfterBuild(Action action) => this.disposeAfterBuild.Add(action);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public T DisposeWithAtlas<T>(T disposable) where T : IDisposable => this.data.Garbage.Add(disposable);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.data.Garbage.Add(gcHandle);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void DisposeWithAtlas(Action action) => this.data.Garbage.Add(action);
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public void FromUiBuilderObsoleteEventHandlers(Action action)
|
||||
{
|
||||
var previousSubstances = new IFontHandleSubstance[this.data.Substances.Count];
|
||||
for (var i = 0; i < previousSubstances.Length; i++)
|
||||
{
|
||||
previousSubstances[i] = this.data.Substances[i].Manager.Substance;
|
||||
this.data.Substances[i].Manager.Substance = this.data.Substances[i];
|
||||
this.data.Substances[i].CreateFontOnAccess = true;
|
||||
this.data.Substances[i].PreBuildToolkitForApi9Compat = this;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
finally
|
||||
{
|
||||
for (var i = 0; i < previousSubstances.Length; i++)
|
||||
{
|
||||
this.data.Substances[i].Manager.Substance = previousSubstances[i];
|
||||
this.data.Substances[i].CreateFontOnAccess = false;
|
||||
this.data.Substances[i].PreBuildToolkitForApi9Compat = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr GetFont(IFontHandle fontHandle)
|
||||
{
|
||||
foreach (var s in this.data.Substances)
|
||||
{
|
||||
var f = s.GetFontPtr(fontHandle);
|
||||
if (!f.IsNull())
|
||||
return f;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr)
|
||||
{
|
||||
this.GlobalScaleExclusions.Add(fontPtr);
|
||||
return fontPtr;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IFontAtlasBuildToolkitPreBuild.IsGlobalScaleIgnored"/>
|
||||
public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) =>
|
||||
this.GlobalScaleExclusions.Contains(fontPtr);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) =>
|
||||
this.data.AddNewTexture(textureWrap, disposeOnError);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory(
|
||||
void* dataPointer,
|
||||
int dataSize,
|
||||
in SafeFontConfig fontConfig,
|
||||
bool freeOnException,
|
||||
string debugTag)
|
||||
{
|
||||
Log.Verbose(
|
||||
"[{name}] 0x{atlas:X}: {funcname}(0x{dataPointer:X}, 0x{dataSize:X}, ...) from {tag}",
|
||||
this.data.Owner?.Name ?? "(error)",
|
||||
(nint)this.NewImAtlas.NativePtr,
|
||||
nameof(this.AddFontFromImGuiHeapAllocatedMemory),
|
||||
(nint)dataPointer,
|
||||
dataSize,
|
||||
debugTag);
|
||||
|
||||
try
|
||||
{
|
||||
fontConfig.ThrowOnInvalidValues();
|
||||
|
||||
var raw = fontConfig.Raw with
|
||||
{
|
||||
FontData = dataPointer,
|
||||
FontDataSize = dataSize,
|
||||
};
|
||||
|
||||
if (fontConfig.GlyphRanges is not { Length: > 0 } ranges)
|
||||
ranges = new ushort[] { 1, 0xFFFE, 0 };
|
||||
|
||||
raw.GlyphRanges = (ushort*)this.DisposeAfterBuild(
|
||||
GCHandle.Alloc(ranges, GCHandleType.Pinned)).AddrOfPinnedObject();
|
||||
|
||||
TrueTypeUtils.CheckImGuiCompatibleOrThrow(raw);
|
||||
|
||||
var font = this.NewImAtlas.AddFont(&raw);
|
||||
|
||||
var dataHash = default(HashCode);
|
||||
dataHash.AddBytes(new(dataPointer, dataSize));
|
||||
var hashIdent = (uint)dataHash.ToHashCode() | ((ulong)dataSize << 32);
|
||||
|
||||
List<(char Left, char Right, float Distance)> pairAdjustments;
|
||||
lock (PairAdjustmentsCache)
|
||||
{
|
||||
if (!PairAdjustmentsCache.TryGetValue(hashIdent, out pairAdjustments))
|
||||
{
|
||||
PairAdjustmentsCache.Add(hashIdent, pairAdjustments = new());
|
||||
try
|
||||
{
|
||||
pairAdjustments.AddRange(TrueTypeUtils.ExtractHorizontalPairAdjustments(raw).ToArray());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// don't care
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var pair in pairAdjustments)
|
||||
{
|
||||
if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Left, raw.GlyphRanges))
|
||||
continue;
|
||||
if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Right, raw.GlyphRanges))
|
||||
continue;
|
||||
|
||||
font.AddKerningPair(pair.Left, pair.Right, pair.Distance * raw.SizePixels);
|
||||
}
|
||||
|
||||
return font;
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (freeOnException)
|
||||
ImGuiNative.igMemFree(dataPointer);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig)
|
||||
{
|
||||
return this.AddFontFromStream(
|
||||
File.OpenRead(path),
|
||||
fontConfig,
|
||||
false,
|
||||
$"{nameof(this.AddFontFromFile)}({path})");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe ImFontPtr AddFontFromStream(
|
||||
Stream stream,
|
||||
in SafeFontConfig fontConfig,
|
||||
bool leaveOpen,
|
||||
string debugTag)
|
||||
{
|
||||
using var streamCloser = leaveOpen ? null : stream;
|
||||
if (!stream.CanSeek)
|
||||
{
|
||||
// There is no need to dispose a MemoryStream.
|
||||
var ms = new MemoryStream();
|
||||
stream.CopyTo(ms);
|
||||
stream = ms;
|
||||
}
|
||||
|
||||
var length = checked((int)(uint)stream.Length);
|
||||
var memory = ImGuiHelpers.AllocateMemory(length);
|
||||
try
|
||||
{
|
||||
stream.ReadExactly(new(memory, length));
|
||||
return this.AddFontFromImGuiHeapAllocatedMemory(
|
||||
memory,
|
||||
length,
|
||||
fontConfig,
|
||||
false,
|
||||
$"{nameof(this.AddFontFromStream)}({debugTag})");
|
||||
}
|
||||
catch
|
||||
{
|
||||
ImGuiNative.igMemFree(memory);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe ImFontPtr AddFontFromMemory(
|
||||
ReadOnlySpan<byte> span,
|
||||
in SafeFontConfig fontConfig,
|
||||
string debugTag)
|
||||
{
|
||||
var length = span.Length;
|
||||
var memory = ImGuiHelpers.AllocateMemory(length);
|
||||
try
|
||||
{
|
||||
span.CopyTo(new(memory, length));
|
||||
return this.AddFontFromImGuiHeapAllocatedMemory(
|
||||
memory,
|
||||
length,
|
||||
fontConfig,
|
||||
false,
|
||||
$"{nameof(this.AddFontFromMemory)}({debugTag})");
|
||||
}
|
||||
catch
|
||||
{
|
||||
ImGuiNative.igMemFree(memory);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges)
|
||||
{
|
||||
ImFontPtr font;
|
||||
glyphRanges ??= this.factory.DefaultGlyphRanges;
|
||||
if (this.factory.UseAxis)
|
||||
{
|
||||
font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default);
|
||||
}
|
||||
else
|
||||
{
|
||||
font = this.AddDalamudAssetFont(
|
||||
DalamudAsset.NotoSansJpMedium,
|
||||
new() { SizePx = sizePx, GlyphRanges = glyphRanges });
|
||||
this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font });
|
||||
}
|
||||
|
||||
this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font });
|
||||
if (this.Font.IsNull())
|
||||
this.Font = font;
|
||||
return font;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig)
|
||||
{
|
||||
if (asset.GetPurpose() != DalamudAssetPurpose.Font)
|
||||
throw new ArgumentOutOfRangeException(nameof(asset), asset, "Must have the purpose of Font.");
|
||||
|
||||
switch (asset)
|
||||
{
|
||||
case DalamudAsset.LodestoneGameSymbol when this.factory.HasGameSymbolsFontFile:
|
||||
return this.factory.AddFont(
|
||||
this,
|
||||
asset,
|
||||
fontConfig with
|
||||
{
|
||||
FontNo = 0,
|
||||
SizePx = (fontConfig.SizePx * 3) / 2,
|
||||
});
|
||||
|
||||
case DalamudAsset.LodestoneGameSymbol when !this.factory.HasGameSymbolsFontFile:
|
||||
{
|
||||
return this.AddGameGlyphs(
|
||||
new(GameFontFamily.Axis, fontConfig.SizePx),
|
||||
fontConfig.GlyphRanges,
|
||||
fontConfig.MergeFont);
|
||||
}
|
||||
|
||||
default:
|
||||
return this.factory.AddFont(
|
||||
this,
|
||||
asset,
|
||||
fontConfig with
|
||||
{
|
||||
FontNo = 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig) => this.AddDalamudAssetFont(
|
||||
DalamudAsset.FontAwesomeFreeSolid,
|
||||
fontConfig with
|
||||
{
|
||||
GlyphRanges = new ushort[] { FontAwesomeIconMin, FontAwesomeIconMax, 0 },
|
||||
});
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig) =>
|
||||
this.AddDalamudAssetFont(
|
||||
DalamudAsset.LodestoneGameSymbol,
|
||||
fontConfig with
|
||||
{
|
||||
GlyphRanges = new ushort[]
|
||||
{
|
||||
GamePrebakedFontHandle.SeIconCharMin,
|
||||
GamePrebakedFontHandle.SeIconCharMax,
|
||||
0,
|
||||
},
|
||||
});
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont) =>
|
||||
this.gameFontHandleSubstance.AttachGameGlyphs(this, mergeFont, gameFontStyle, glyphRanges);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig)
|
||||
{
|
||||
var dalamudConfiguration = Service<DalamudConfiguration>.Get();
|
||||
if (dalamudConfiguration.EffectiveLanguage == "ko"
|
||||
|| Service<DalamudIme>.GetNullable()?.EncounteredHangul is true)
|
||||
{
|
||||
this.AddDalamudAssetFont(
|
||||
DalamudAsset.NotoSansKrRegular,
|
||||
fontConfig with
|
||||
{
|
||||
GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom(
|
||||
UnicodeRanges.HangulJamo,
|
||||
UnicodeRanges.HangulCompatibilityJamo,
|
||||
UnicodeRanges.HangulSyllables,
|
||||
UnicodeRanges.HangulJamoExtendedA,
|
||||
UnicodeRanges.HangulJamoExtendedB),
|
||||
});
|
||||
}
|
||||
|
||||
var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
|
||||
var fontPathChs = Path.Combine(windowsDir, "Fonts", "msyh.ttc");
|
||||
if (!File.Exists(fontPathChs))
|
||||
fontPathChs = null;
|
||||
|
||||
var fontPathCht = Path.Combine(windowsDir, "Fonts", "msjh.ttc");
|
||||
if (!File.Exists(fontPathCht))
|
||||
fontPathCht = null;
|
||||
|
||||
if (fontPathCht != null && Service<DalamudConfiguration>.Get().EffectiveLanguage == "tw")
|
||||
{
|
||||
this.AddFontFromFile(fontPathCht, fontConfig with
|
||||
{
|
||||
GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom(
|
||||
UnicodeRanges.CjkUnifiedIdeographs,
|
||||
UnicodeRanges.CjkUnifiedIdeographsExtensionA),
|
||||
});
|
||||
}
|
||||
else if (fontPathChs != null && (Service<DalamudConfiguration>.Get().EffectiveLanguage == "zh"
|
||||
|| Service<DalamudIme>.GetNullable()?.EncounteredHan is true))
|
||||
{
|
||||
this.AddFontFromFile(fontPathChs, fontConfig with
|
||||
{
|
||||
GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom(
|
||||
UnicodeRanges.CjkUnifiedIdeographs,
|
||||
UnicodeRanges.CjkUnifiedIdeographsExtensionA),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void PreBuildSubstances()
|
||||
{
|
||||
foreach (var substance in this.data.Substances)
|
||||
substance.OnPreBuild(this);
|
||||
foreach (var substance in this.data.Substances)
|
||||
substance.OnPreBuildCleanup(this);
|
||||
}
|
||||
|
||||
public unsafe void PreBuild()
|
||||
{
|
||||
var configData = this.data.ConfigData;
|
||||
foreach (ref var config in configData.DataSpan)
|
||||
{
|
||||
if (this.GlobalScaleExclusions.Contains(new(config.DstFont)))
|
||||
continue;
|
||||
|
||||
config.SizePixels *= this.Scale;
|
||||
|
||||
config.GlyphMaxAdvanceX *= this.Scale;
|
||||
if (float.IsInfinity(config.GlyphMaxAdvanceX))
|
||||
config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue;
|
||||
|
||||
config.GlyphMinAdvanceX *= this.Scale;
|
||||
if (float.IsInfinity(config.GlyphMinAdvanceX))
|
||||
config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue;
|
||||
|
||||
config.GlyphOffset *= this.Scale;
|
||||
}
|
||||
}
|
||||
|
||||
public void DoBuild()
|
||||
{
|
||||
// ImGui will call AddFontDefault() on Build() call.
|
||||
// AddFontDefault() will reliably crash, when invoked multithreaded.
|
||||
// We add a dummy font to prevent that.
|
||||
if (this.data.ConfigData.Length == 0)
|
||||
{
|
||||
this.AddDalamudAssetFont(
|
||||
DalamudAsset.NotoSansJpMedium,
|
||||
new() { GlyphRanges = new ushort[] { ' ', ' ', '\0' }, SizePx = 1 });
|
||||
}
|
||||
|
||||
if (!this.NewImAtlas.Build())
|
||||
throw new InvalidOperationException("ImFontAtlas.Build failed");
|
||||
|
||||
this.BuildStep = FontAtlasBuildStep.PostBuild;
|
||||
}
|
||||
|
||||
public unsafe void PostBuild()
|
||||
{
|
||||
var scale = this.Scale;
|
||||
foreach (ref var font in this.Fonts.DataSpan)
|
||||
{
|
||||
if (!this.GlobalScaleExclusions.Contains(font))
|
||||
font.AdjustGlyphMetrics(1 / scale, 1 / scale);
|
||||
|
||||
foreach (var c in FallbackCodepoints)
|
||||
{
|
||||
var g = font.FindGlyphNoFallback(c);
|
||||
if (g.NativePtr == null)
|
||||
continue;
|
||||
|
||||
font.UpdateFallbackChar(c);
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (var c in EllipsisCodepoints)
|
||||
{
|
||||
var g = font.FindGlyphNoFallback(c);
|
||||
if (g.NativePtr == null)
|
||||
continue;
|
||||
|
||||
font.EllipsisChar = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void PostBuildSubstances()
|
||||
{
|
||||
foreach (var substance in this.data.Substances)
|
||||
substance.OnPostBuild(this);
|
||||
}
|
||||
|
||||
public unsafe void UploadTextures()
|
||||
{
|
||||
var buf = Array.Empty<byte>();
|
||||
try
|
||||
{
|
||||
var use4 = this.factory.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm);
|
||||
var bpp = use4 ? 2 : 4;
|
||||
var width = this.NewImAtlas.TexWidth;
|
||||
var height = this.NewImAtlas.TexHeight;
|
||||
foreach (ref var texture in this.data.ImTextures.DataSpan)
|
||||
{
|
||||
if (texture.TexID != 0)
|
||||
{
|
||||
// Nothing to do
|
||||
}
|
||||
else if (texture.TexPixelsRGBA32 is not null)
|
||||
{
|
||||
var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat(
|
||||
new(texture.TexPixelsRGBA32, width * height * 4),
|
||||
width * 4,
|
||||
width,
|
||||
height,
|
||||
use4 ? Format.B4G4R4A4_UNorm : Format.R8G8B8A8_UNorm);
|
||||
this.data.AddExistingTexture(wrap);
|
||||
texture.TexID = wrap.ImGuiHandle;
|
||||
}
|
||||
else if (texture.TexPixelsAlpha8 is not null)
|
||||
{
|
||||
var numPixels = width * height;
|
||||
if (buf.Length < numPixels * bpp)
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buf);
|
||||
buf = ArrayPool<byte>.Shared.Rent(numPixels * bpp);
|
||||
}
|
||||
|
||||
fixed (void* pBuf = buf)
|
||||
{
|
||||
var sourcePtr = texture.TexPixelsAlpha8;
|
||||
if (use4)
|
||||
{
|
||||
var target = (ushort*)pBuf;
|
||||
while (numPixels-- > 0)
|
||||
{
|
||||
*target = (ushort)((*sourcePtr << 8) | 0x0FFF);
|
||||
target++;
|
||||
sourcePtr++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var target = (uint*)pBuf;
|
||||
while (numPixels-- > 0)
|
||||
{
|
||||
*target = (uint)((*sourcePtr << 24) | 0x00FFFFFF);
|
||||
target++;
|
||||
sourcePtr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat(
|
||||
buf,
|
||||
width * bpp,
|
||||
width,
|
||||
height,
|
||||
use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm);
|
||||
this.data.AddExistingTexture(wrap);
|
||||
texture.TexID = wrap.ImGuiHandle;
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning(
|
||||
"[{name}]: TexID, TexPixelsRGBA32, and TexPixelsAlpha8 are all null",
|
||||
this.data.Owner?.Name ?? "(error)");
|
||||
}
|
||||
|
||||
if (texture.TexPixelsRGBA32 is not null)
|
||||
ImGuiNative.igMemFree(texture.TexPixelsRGBA32);
|
||||
if (texture.TexPixelsAlpha8 is not null)
|
||||
ImGuiNative.igMemFree(texture.TexPixelsAlpha8);
|
||||
texture.TexPixelsRGBA32 = null;
|
||||
texture.TexPixelsAlpha8 = null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe void CopyGlyphsAcrossFonts(
|
||||
ImFontPtr source,
|
||||
ImFontPtr target,
|
||||
bool missingOnly,
|
||||
bool rebuildLookupTable = true,
|
||||
char rangeLow = ' ',
|
||||
char rangeHigh = '\uFFFE')
|
||||
{
|
||||
var sourceFound = false;
|
||||
var targetFound = false;
|
||||
foreach (var f in this.Fonts)
|
||||
{
|
||||
sourceFound |= f.NativePtr == source.NativePtr;
|
||||
targetFound |= f.NativePtr == target.NativePtr;
|
||||
}
|
||||
|
||||
if (sourceFound && targetFound)
|
||||
{
|
||||
ImGuiHelpers.CopyGlyphsAcrossFonts(
|
||||
source,
|
||||
target,
|
||||
missingOnly,
|
||||
false,
|
||||
rangeLow,
|
||||
rangeHigh);
|
||||
if (rebuildLookupTable)
|
||||
this.BuildLookupTable(target);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe void BuildLookupTable(ImFontPtr font)
|
||||
{
|
||||
// Need to clear previous Fallback pointers before BuildLookupTable, or it may crash
|
||||
font.NativePtr->FallbackGlyph = null;
|
||||
font.NativePtr->FallbackHotData = null;
|
||||
font.BuildLookupTable();
|
||||
|
||||
// Need to fix our custom ImGui, so that imgui_widgets.cpp:3656 stops thinking
|
||||
// Codepoint < FallbackHotData.size always means that it's not fallback char.
|
||||
// Otherwise, having a fallback character in ImGui.InputText gets strange.
|
||||
var indexedHotData = font.IndexedHotDataWrapped();
|
||||
var indexLookup = font.IndexLookupWrapped();
|
||||
ref var fallbackHotData = ref *(ImGuiHelpers.ImFontGlyphHotDataReal*)font.NativePtr->FallbackHotData;
|
||||
for (var codepoint = 0; codepoint < indexedHotData.Length; codepoint++)
|
||||
{
|
||||
if (indexLookup[codepoint] == ushort.MaxValue)
|
||||
{
|
||||
indexedHotData[codepoint].AdvanceX = fallbackHotData.AdvanceX;
|
||||
indexedHotData[codepoint].OccupiedWidth = fallbackHotData.OccupiedWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,796 @@
|
|||
// #define VeryVerboseLog
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Logging.Internal;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Standalone font atlas.
|
||||
/// </summary>
|
||||
internal sealed partial class FontAtlasFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Fallback codepoints for ImFont.
|
||||
/// </summary>
|
||||
public const string FallbackCodepoints = "\u3013\uFFFD?-";
|
||||
|
||||
/// <summary>
|
||||
/// Ellipsis codepoints for ImFont.
|
||||
/// </summary>
|
||||
public const string EllipsisCodepoints = "\u2026\u0085";
|
||||
|
||||
/// <summary>
|
||||
/// If set, disables concurrent font build operation.
|
||||
/// </summary>
|
||||
private static readonly object? NoConcurrentBuildOperationLock = null; // new();
|
||||
|
||||
private static readonly ModuleLog Log = new(nameof(FontAtlasFactory));
|
||||
|
||||
private static readonly Task<FontAtlasBuiltData> EmptyTask = Task.FromResult(default(FontAtlasBuiltData));
|
||||
|
||||
private class FontAtlasBuiltData : IRefCountable
|
||||
{
|
||||
private readonly List<IDalamudTextureWrap> wraps;
|
||||
private readonly List<IFontHandleSubstance> substances;
|
||||
|
||||
private int refCount;
|
||||
|
||||
public unsafe FontAtlasBuiltData(DalamudFontAtlas owner, float scale)
|
||||
{
|
||||
this.Owner = owner;
|
||||
this.Scale = scale;
|
||||
this.Garbage = new();
|
||||
this.refCount = 1;
|
||||
|
||||
try
|
||||
{
|
||||
var substancesList = this.substances = new();
|
||||
this.Garbage.Add(() => substancesList.Clear());
|
||||
|
||||
var wrapsCopy = this.wraps = new();
|
||||
this.Garbage.Add(() => wrapsCopy.Clear());
|
||||
|
||||
var atlasPtr = ImGuiNative.ImFontAtlas_ImFontAtlas();
|
||||
this.Atlas = atlasPtr;
|
||||
if (this.Atlas.NativePtr is null)
|
||||
throw new OutOfMemoryException($"Failed to allocate a new {nameof(ImFontAtlas)}.");
|
||||
|
||||
this.Garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr));
|
||||
this.IsBuildInProgress = true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
this.Garbage.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public DalamudFontAtlas? Owner { get; }
|
||||
|
||||
public ImFontAtlasPtr Atlas { get; }
|
||||
|
||||
public float Scale { get; }
|
||||
|
||||
public bool IsBuildInProgress { get; set; }
|
||||
|
||||
public DisposeSafety.ScopedFinalizer Garbage { get; }
|
||||
|
||||
public ImVectorWrapper<ImFontPtr> Fonts => this.Atlas.FontsWrapped();
|
||||
|
||||
public ImVectorWrapper<ImFontConfig> ConfigData => this.Atlas.ConfigDataWrapped();
|
||||
|
||||
public ImVectorWrapper<ImFontAtlasTexture> ImTextures => this.Atlas.TexturesWrapped();
|
||||
|
||||
public IReadOnlyList<IDalamudTextureWrap> Wraps => this.wraps;
|
||||
|
||||
public IReadOnlyList<IFontHandleSubstance> Substances => this.substances;
|
||||
|
||||
public void InitialAddSubstance(IFontHandleSubstance substance) =>
|
||||
this.substances.Add(this.Garbage.Add(substance));
|
||||
|
||||
public void AddExistingTexture(IDalamudTextureWrap wrap)
|
||||
{
|
||||
if (this.wraps is null)
|
||||
throw new ObjectDisposedException(nameof(FontAtlasBuiltData));
|
||||
|
||||
this.wraps.Add(this.Garbage.Add(wrap));
|
||||
}
|
||||
|
||||
public int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError)
|
||||
{
|
||||
if (this.wraps is null)
|
||||
throw new ObjectDisposedException(nameof(FontAtlasBuiltData));
|
||||
|
||||
var handle = wrap.ImGuiHandle;
|
||||
var index = this.ImTextures.IndexOf(x => x.TexID == handle);
|
||||
if (index == -1)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.wraps.EnsureCapacity(this.wraps.Count + 1);
|
||||
this.ImTextures.EnsureCapacityExponential(this.ImTextures.Length + 1);
|
||||
|
||||
index = this.ImTextures.Length;
|
||||
this.wraps.Add(this.Garbage.Add(wrap));
|
||||
this.ImTextures.Add(new() { TexID = handle });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (disposeOnError)
|
||||
wrap.Dispose();
|
||||
|
||||
if (this.wraps.Count != this.ImTextures.Length)
|
||||
{
|
||||
Log.Error(
|
||||
e,
|
||||
"{name} failed, and {wraps} and {imtextures} have different number of items",
|
||||
nameof(this.AddNewTexture),
|
||||
nameof(this.Wraps),
|
||||
nameof(this.ImTextures));
|
||||
|
||||
if (this.wraps.Count > 0 && this.wraps[^1] == wrap)
|
||||
this.wraps.RemoveAt(this.wraps.Count - 1);
|
||||
if (this.ImTextures.Length > 0 && this.ImTextures[^1].TexID == handle)
|
||||
this.ImTextures.RemoveAt(this.ImTextures.Length - 1);
|
||||
|
||||
if (this.wraps.Count != this.ImTextures.Length)
|
||||
Log.Fatal("^ Failed to undo due to an internal inconsistency; embrace for a crash");
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
public int AddRef() => IRefCountable.AlterRefCount(1, ref this.refCount, out var newRefCount) switch
|
||||
{
|
||||
IRefCountable.RefCountResult.StillAlive => newRefCount,
|
||||
IRefCountable.RefCountResult.AlreadyDisposed =>
|
||||
throw new ObjectDisposedException(nameof(FontAtlasBuiltData)),
|
||||
IRefCountable.RefCountResult.FinalRelease => throw new InvalidOperationException(),
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
|
||||
public int Release()
|
||||
{
|
||||
switch (IRefCountable.AlterRefCount(-1, ref this.refCount, out var newRefCount))
|
||||
{
|
||||
case IRefCountable.RefCountResult.StillAlive:
|
||||
return newRefCount;
|
||||
|
||||
case IRefCountable.RefCountResult.FinalRelease:
|
||||
#if VeryVerboseLog
|
||||
Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "<?>", (nint)this.Atlas.NativePtr);
|
||||
#endif
|
||||
|
||||
if (this.IsBuildInProgress)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
Log.Error(
|
||||
"[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; disposing later.\n" +
|
||||
"Stack:\n{trace}",
|
||||
this.Owner?.Name ?? "<?>",
|
||||
(nint)this.Atlas.NativePtr,
|
||||
new StackTrace());
|
||||
}
|
||||
|
||||
Task.Run(
|
||||
async () =>
|
||||
{
|
||||
while (this.IsBuildInProgress)
|
||||
await Task.Delay(100);
|
||||
this.Garbage.Dispose();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
this.Garbage.Dispose();
|
||||
}
|
||||
|
||||
return newRefCount;
|
||||
|
||||
case IRefCountable.RefCountResult.AlreadyDisposed:
|
||||
throw new ObjectDisposedException(nameof(FontAtlasBuiltData));
|
||||
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
public BuildToolkit CreateToolkit(FontAtlasFactory factory, bool isAsync)
|
||||
{
|
||||
var axisSubstance = this.Substances.OfType<GamePrebakedFontHandle.HandleSubstance>().Single();
|
||||
return new(factory, this, axisSubstance, isAsync) { BuildStep = FontAtlasBuildStep.PreBuild };
|
||||
}
|
||||
}
|
||||
|
||||
private class DalamudFontAtlas : IFontAtlas, DisposeSafety.IDisposeCallback
|
||||
{
|
||||
private readonly DisposeSafety.ScopedFinalizer disposables = new();
|
||||
private readonly FontAtlasFactory factory;
|
||||
private readonly DelegateFontHandle.HandleManager delegateFontHandleManager;
|
||||
private readonly GamePrebakedFontHandle.HandleManager gameFontHandleManager;
|
||||
private readonly IFontHandleManager[] fontHandleManagers;
|
||||
|
||||
private readonly object syncRoot = new();
|
||||
|
||||
private Task<FontAtlasBuiltData?> buildTask = EmptyTask;
|
||||
private FontAtlasBuiltData? builtData;
|
||||
|
||||
private int buildSuppressionCounter;
|
||||
private bool buildSuppressionSuppressed;
|
||||
|
||||
private int buildIndex;
|
||||
private bool buildQueued;
|
||||
private bool disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DalamudFontAtlas"/> class.
|
||||
/// </summary>
|
||||
/// <param name="factory">The factory.</param>
|
||||
/// <param name="atlasName">Name of atlas, for debugging and logging purposes.</param>
|
||||
/// <param name="autoRebuildMode">Specify how to auto rebuild.</param>
|
||||
/// <param name="isGlobalScaled">Whether the fonts in the atlas are under the effect of global scale.</param>
|
||||
public DalamudFontAtlas(
|
||||
FontAtlasFactory factory,
|
||||
string atlasName,
|
||||
FontAtlasAutoRebuildMode autoRebuildMode,
|
||||
bool isGlobalScaled)
|
||||
{
|
||||
this.IsGlobalScaled = isGlobalScaled;
|
||||
try
|
||||
{
|
||||
this.factory = factory;
|
||||
this.AutoRebuildMode = autoRebuildMode;
|
||||
this.Name = atlasName;
|
||||
|
||||
this.factory.InterfaceManager.AfterBuildFonts += this.OnRebuildRecommend;
|
||||
this.disposables.Add(() => this.factory.InterfaceManager.AfterBuildFonts -= this.OnRebuildRecommend);
|
||||
|
||||
this.fontHandleManagers = new IFontHandleManager[]
|
||||
{
|
||||
this.delegateFontHandleManager = this.disposables.Add(
|
||||
new DelegateFontHandle.HandleManager(atlasName)),
|
||||
this.gameFontHandleManager = this.disposables.Add(
|
||||
new GamePrebakedFontHandle.HandleManager(atlasName, factory)),
|
||||
};
|
||||
foreach (var fhm in this.fontHandleManagers)
|
||||
fhm.RebuildRecommend += this.OnRebuildRecommend;
|
||||
}
|
||||
catch
|
||||
{
|
||||
this.disposables.Dispose();
|
||||
throw;
|
||||
}
|
||||
|
||||
this.factory.SceneTask.ContinueWith(
|
||||
r =>
|
||||
{
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
if (this.disposed)
|
||||
return;
|
||||
|
||||
r.Result.OnNewRenderFrame += this.ImGuiSceneOnNewRenderFrame;
|
||||
this.disposables.Add(() => r.Result.OnNewRenderFrame -= this.ImGuiSceneOnNewRenderFrame);
|
||||
}
|
||||
|
||||
if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame)
|
||||
this.BuildFontsOnNextFrame();
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="DalamudFontAtlas"/> class.
|
||||
/// </summary>
|
||||
~DalamudFontAtlas()
|
||||
{
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
this.buildTask.ToDisposableIgnoreExceptions().Dispose();
|
||||
this.builtData?.Release();
|
||||
this.builtData = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event FontAtlasBuildStepDelegate? BuildStepChange;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action? RebuildRecommend;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action<DisposeSafety.IDisposeCallback>? BeforeDispose;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action<DisposeSafety.IDisposeCallback, Exception?>? AfterDispose;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FontAtlasAutoRebuildMode AutoRebuildMode { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontAtlasPtr ImAtlas
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (this.syncRoot)
|
||||
return this.builtData?.Atlas ?? default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task BuildTask => this.buildTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool HasBuiltAtlas => !(this.builtData?.Atlas.IsNull() ?? true);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsGlobalScaled { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.disposed)
|
||||
return;
|
||||
|
||||
this.BeforeDispose?.InvokeSafely(this);
|
||||
|
||||
try
|
||||
{
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
this.disposed = true;
|
||||
this.buildTask.ToDisposableIgnoreExceptions().Dispose();
|
||||
this.buildTask = EmptyTask;
|
||||
this.disposables.Add(this.builtData);
|
||||
this.builtData = default;
|
||||
this.disposables.Dispose();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
this.AfterDispose?.Invoke(this, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.AfterDispose?.Invoke(this, e);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDisposable SuppressAutoRebuild()
|
||||
{
|
||||
this.buildSuppressionCounter++;
|
||||
return Disposable.Create(
|
||||
() =>
|
||||
{
|
||||
this.buildSuppressionCounter--;
|
||||
if (this.buildSuppressionSuppressed)
|
||||
this.OnRebuildRecommend();
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) =>
|
||||
this.delegateFontHandleManager.NewFontHandle(buildStepDelegate);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void BuildFontsOnNextFrame()
|
||||
{
|
||||
if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(this.BuildFontsOnNextFrame)} cannot be used when " +
|
||||
$"{nameof(this.AutoRebuildMode)} is set to " +
|
||||
$"{nameof(FontAtlasAutoRebuildMode.Async)}.");
|
||||
}
|
||||
|
||||
if (!this.buildTask.IsCompleted || this.buildQueued)
|
||||
return;
|
||||
|
||||
#if VeryVerboseLog
|
||||
Log.Verbose("[{name}] Queueing from {source}.", this.Name, nameof(this.BuildFontsOnNextFrame));
|
||||
#endif
|
||||
|
||||
this.buildQueued = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void BuildFontsImmediately()
|
||||
{
|
||||
#if VeryVerboseLog
|
||||
Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsImmediately));
|
||||
#endif
|
||||
|
||||
if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(this.BuildFontsImmediately)} cannot be used when " +
|
||||
$"{nameof(this.AutoRebuildMode)} is set to " +
|
||||
$"{nameof(FontAtlasAutoRebuildMode.Async)}.");
|
||||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<FontAtlasBuiltData>();
|
||||
try
|
||||
{
|
||||
var rebuildIndex = Interlocked.Increment(ref this.buildIndex);
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
if (!this.buildTask.IsCompleted)
|
||||
throw new InvalidOperationException("Font rebuild is already in progress.");
|
||||
|
||||
this.buildTask = tcs.Task;
|
||||
}
|
||||
|
||||
#if VeryVerboseLog
|
||||
Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsImmediately));
|
||||
#endif
|
||||
|
||||
var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f;
|
||||
var r = this.RebuildFontsPrivate(false, scale);
|
||||
r.Wait();
|
||||
if (r.IsCompletedSuccessfully)
|
||||
{
|
||||
this.PromoteBuiltData(rebuildIndex, r.Result, nameof(this.BuildFontsImmediately));
|
||||
tcs.SetResult(r.Result);
|
||||
}
|
||||
else if ((r.Exception?.InnerException ?? r.Exception) is { } taskException)
|
||||
{
|
||||
ExceptionDispatchInfo.Capture(taskException).Throw();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new OperationCanceledException();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
tcs.SetException(e);
|
||||
Log.Error(e, "[{name}] Failed to build fonts.", this.Name);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task BuildFontsAsync()
|
||||
{
|
||||
#if VeryVerboseLog
|
||||
Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsAsync));
|
||||
#endif
|
||||
|
||||
if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"{nameof(this.BuildFontsAsync)} cannot be used when " +
|
||||
$"{nameof(this.AutoRebuildMode)} is set to " +
|
||||
$"{nameof(FontAtlasAutoRebuildMode.OnNewFrame)}.");
|
||||
}
|
||||
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f;
|
||||
var rebuildIndex = Interlocked.Increment(ref this.buildIndex);
|
||||
return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap();
|
||||
|
||||
async Task<FontAtlasBuiltData?> BuildInner(Task<FontAtlasBuiltData> unused)
|
||||
{
|
||||
Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsAsync));
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
if (this.buildIndex != rebuildIndex)
|
||||
return null;
|
||||
}
|
||||
|
||||
var res = await this.RebuildFontsPrivate(true, scale);
|
||||
if (res.Atlas.IsNull())
|
||||
return res;
|
||||
|
||||
this.PromoteBuiltData(rebuildIndex, res, nameof(this.BuildFontsAsync));
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void PromoteBuiltData(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source)
|
||||
{
|
||||
// Capture the locks inside the lock block, so that the fonts are guaranteed to be the ones just built.
|
||||
var fontsAndLocks = new List<(FontHandle FontHandle, ILockedImFont Lock)>();
|
||||
using var garbage = new DisposeSafety.ScopedFinalizer();
|
||||
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
if (this.buildIndex != rebuildIndex)
|
||||
{
|
||||
data.ExplicitDisposeIgnoreExceptions();
|
||||
return;
|
||||
}
|
||||
|
||||
var prevBuiltData = this.builtData;
|
||||
this.builtData = data;
|
||||
prevBuiltData.ExplicitDisposeIgnoreExceptions();
|
||||
|
||||
this.buildTask = EmptyTask;
|
||||
fontsAndLocks.EnsureCapacity(data.Substances.Sum(x => x.RelevantHandles.Count));
|
||||
foreach (var substance in data.Substances)
|
||||
{
|
||||
substance.Manager.Substance = substance;
|
||||
foreach (var fontHandle in substance.RelevantHandles)
|
||||
{
|
||||
substance.DataRoot.AddRef();
|
||||
var locked = new LockedImFont(
|
||||
substance.GetFontPtr(fontHandle),
|
||||
substance.DataRoot);
|
||||
fontsAndLocks.Add((fontHandle, garbage.Add(locked)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (fontHandle, lockedFont) in fontsAndLocks)
|
||||
fontHandle.InvokeImFontChanged(lockedFont);
|
||||
|
||||
#if VeryVerboseLog
|
||||
Log.Verbose("[{name}] Built from {source}.", this.Name, source);
|
||||
#endif
|
||||
}
|
||||
|
||||
private void ImGuiSceneOnNewRenderFrame()
|
||||
{
|
||||
if (!this.buildQueued)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (this.AutoRebuildMode != FontAtlasAutoRebuildMode.Async)
|
||||
this.BuildFontsImmediately();
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.buildQueued = false;
|
||||
}
|
||||
}
|
||||
|
||||
private Task<FontAtlasBuiltData> RebuildFontsPrivate(bool isAsync, float scale)
|
||||
{
|
||||
if (NoConcurrentBuildOperationLock is null)
|
||||
return this.RebuildFontsPrivateReal(isAsync, scale);
|
||||
lock (NoConcurrentBuildOperationLock)
|
||||
return this.RebuildFontsPrivateReal(isAsync, scale);
|
||||
}
|
||||
|
||||
private async Task<FontAtlasBuiltData> RebuildFontsPrivateReal(bool isAsync, float scale)
|
||||
{
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
// this lock ensures that this.buildTask is properly set.
|
||||
}
|
||||
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
|
||||
FontAtlasBuiltData? res = null;
|
||||
nint atlasPtr = 0;
|
||||
BuildToolkit? toolkit = null;
|
||||
try
|
||||
{
|
||||
res = new(this, scale);
|
||||
foreach (var fhm in this.fontHandleManagers)
|
||||
res.InitialAddSubstance(fhm.NewSubstance(res));
|
||||
unsafe
|
||||
{
|
||||
atlasPtr = (nint)res.Atlas.NativePtr;
|
||||
}
|
||||
|
||||
Log.Verbose(
|
||||
"[{name}:{functionname}] 0x{ptr:X}: PreBuild (at {sw}ms)",
|
||||
this.Name,
|
||||
nameof(this.RebuildFontsPrivateReal),
|
||||
atlasPtr,
|
||||
sw.ElapsedMilliseconds);
|
||||
|
||||
toolkit = res.CreateToolkit(this.factory, isAsync);
|
||||
this.BuildStepChange?.Invoke(toolkit);
|
||||
toolkit.PreBuildSubstances();
|
||||
toolkit.PreBuild();
|
||||
|
||||
// Prevent NewImAtlas.ConfigData[].DstFont pointing to a font not owned by the new atlas,
|
||||
// by making it add a font with default configuration first instead.
|
||||
if (!ValidateMergeFontReferences(default))
|
||||
{
|
||||
Log.Warning(
|
||||
"[{name}:{functionname}] 0x{ptr:X}: refering to fonts outside the new atlas; " +
|
||||
"adding a default font, and using that as the merge target.",
|
||||
this.Name,
|
||||
nameof(this.RebuildFontsPrivateReal),
|
||||
atlasPtr);
|
||||
|
||||
res.IsBuildInProgress = false;
|
||||
toolkit.Dispose();
|
||||
res.Release();
|
||||
|
||||
res = new(this, scale);
|
||||
foreach (var fhm in this.fontHandleManagers)
|
||||
res.InitialAddSubstance(fhm.NewSubstance(res));
|
||||
unsafe
|
||||
{
|
||||
atlasPtr = (nint)res.Atlas.NativePtr;
|
||||
}
|
||||
|
||||
toolkit = res.CreateToolkit(this.factory, isAsync);
|
||||
|
||||
// PreBuildSubstances deals with toolkit.Add... function family. Do this first.
|
||||
var defaultFont = toolkit.AddDalamudDefaultFont(InterfaceManager.DefaultFontSizePx, null);
|
||||
|
||||
this.BuildStepChange?.Invoke(toolkit);
|
||||
toolkit.PreBuildSubstances();
|
||||
toolkit.PreBuild();
|
||||
|
||||
_ = ValidateMergeFontReferences(defaultFont);
|
||||
}
|
||||
|
||||
#if VeryVerboseLog
|
||||
Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: Build (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds);
|
||||
#endif
|
||||
|
||||
toolkit.DoBuild();
|
||||
|
||||
#if VeryVerboseLog
|
||||
Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: PostBuild (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds);
|
||||
#endif
|
||||
|
||||
toolkit.PostBuild();
|
||||
toolkit.PostBuildSubstances();
|
||||
this.BuildStepChange?.Invoke(toolkit);
|
||||
|
||||
foreach (var font in toolkit.Fonts)
|
||||
toolkit.BuildLookupTable(font);
|
||||
|
||||
if (this.factory.SceneTask is { IsCompleted: false } sceneTask)
|
||||
{
|
||||
Log.Verbose(
|
||||
"[{name}:{functionname}] 0x{ptr:X}: await SceneTask (at {sw}ms)",
|
||||
this.Name,
|
||||
nameof(this.RebuildFontsPrivateReal),
|
||||
atlasPtr,
|
||||
sw.ElapsedMilliseconds);
|
||||
await sceneTask.ConfigureAwait(!isAsync);
|
||||
}
|
||||
|
||||
#if VeryVerboseLog
|
||||
Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: UploadTextures (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds);
|
||||
#endif
|
||||
toolkit.UploadTextures();
|
||||
|
||||
Log.Verbose(
|
||||
"[{name}:{functionname}] 0x{ptr:X}: Complete (at {sw}ms)",
|
||||
this.Name,
|
||||
nameof(this.RebuildFontsPrivateReal),
|
||||
atlasPtr,
|
||||
sw.ElapsedMilliseconds);
|
||||
|
||||
res.IsBuildInProgress = false;
|
||||
return res;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(
|
||||
e,
|
||||
"[{name}:{functionname}] 0x{ptr:X}: Failed (at {sw}ms)",
|
||||
this.Name,
|
||||
nameof(this.RebuildFontsPrivateReal),
|
||||
atlasPtr,
|
||||
sw.ElapsedMilliseconds);
|
||||
if (res is not null)
|
||||
{
|
||||
res.IsBuildInProgress = false;
|
||||
res.Release();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// RS is being dumb
|
||||
// ReSharper disable once ConstantConditionalAccessQualifier
|
||||
toolkit?.Dispose();
|
||||
this.buildQueued = false;
|
||||
}
|
||||
|
||||
unsafe bool ValidateMergeFontReferences(ImFontPtr replacementDstFont)
|
||||
{
|
||||
var correct = true;
|
||||
foreach (ref var configData in toolkit.NewImAtlas.ConfigDataWrapped().DataSpan)
|
||||
{
|
||||
var found = false;
|
||||
foreach (ref var font in toolkit.Fonts.DataSpan)
|
||||
{
|
||||
if (configData.DstFont == font)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
correct = false;
|
||||
configData.DstFont = replacementDstFont;
|
||||
}
|
||||
}
|
||||
|
||||
return correct;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRebuildRecommend()
|
||||
{
|
||||
if (this.disposed)
|
||||
return;
|
||||
|
||||
if (this.buildSuppressionCounter > 0)
|
||||
{
|
||||
this.buildSuppressionSuppressed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.buildSuppressionSuppressed = false;
|
||||
this.factory.Framework.RunOnFrameworkThread(
|
||||
() =>
|
||||
{
|
||||
this.RebuildRecommend?.InvokeSafely();
|
||||
|
||||
switch (this.AutoRebuildMode)
|
||||
{
|
||||
case FontAtlasAutoRebuildMode.Async:
|
||||
_ = this.BuildFontsAsync();
|
||||
break;
|
||||
case FontAtlasAutoRebuildMode.OnNewFrame:
|
||||
this.BuildFontsOnNextFrame();
|
||||
break;
|
||||
case FontAtlasAutoRebuildMode.Disable:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
368
Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs
Normal file
368
Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Data;
|
||||
using Dalamud.Game;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Storage.Assets;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using ImGuiScene;
|
||||
|
||||
using Lumina.Data.Files;
|
||||
|
||||
using SharpDX;
|
||||
using SharpDX.Direct3D11;
|
||||
using SharpDX.DXGI;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Factory for the implementation of <see cref="IFontAtlas"/>.
|
||||
/// </summary>
|
||||
[ServiceManager.BlockingEarlyLoadedService]
|
||||
internal sealed partial class FontAtlasFactory
|
||||
: IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable
|
||||
{
|
||||
private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new();
|
||||
private readonly CancellationTokenSource cancellationTokenSource = new();
|
||||
private readonly IReadOnlyDictionary<GameFontFamilyAndSize, Task<byte[]>> fdtFiles;
|
||||
private readonly IReadOnlyDictionary<string, Task<Task<TexFile>[]>> texFiles;
|
||||
private readonly IReadOnlyDictionary<string, Task<IDalamudTextureWrap?[]>> prebakedTextureWraps;
|
||||
private readonly Task<ushort[]> defaultGlyphRanges;
|
||||
private readonly DalamudAssetManager dalamudAssetManager;
|
||||
|
||||
[ServiceManager.ServiceConstructor]
|
||||
private FontAtlasFactory(
|
||||
DataManager dataManager,
|
||||
Framework framework,
|
||||
InterfaceManager interfaceManager,
|
||||
DalamudAssetManager dalamudAssetManager)
|
||||
{
|
||||
this.Framework = framework;
|
||||
this.InterfaceManager = interfaceManager;
|
||||
this.dalamudAssetManager = dalamudAssetManager;
|
||||
this.SceneTask = Service<InterfaceManager.InterfaceManagerWithScene>
|
||||
.GetAsync()
|
||||
.ContinueWith(r => r.Result.Manager.Scene);
|
||||
|
||||
var gffasInfo = Enum.GetValues<GameFontFamilyAndSize>()
|
||||
.Select(
|
||||
x =>
|
||||
(
|
||||
Font: x,
|
||||
Attr: x.GetAttribute<GameFontFamilyAndSizeAttribute>()))
|
||||
.Where(x => x.Attr is not null)
|
||||
.ToArray();
|
||||
var texPaths = gffasInfo.Select(x => x.Attr.TexPathFormat).Distinct().ToArray();
|
||||
|
||||
this.fdtFiles = gffasInfo.ToImmutableDictionary(
|
||||
x => x.Font,
|
||||
x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data));
|
||||
var channelCountsTask = texPaths.ToImmutableDictionary(
|
||||
x => x,
|
||||
x => Task.WhenAll(
|
||||
gffasInfo.Where(y => y.Attr.TexPathFormat == x)
|
||||
.Select(y => this.fdtFiles[y.Font]))
|
||||
.ContinueWith(
|
||||
files => 1 + files.Result.Max(
|
||||
file =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
using var pin = file.AsMemory().Pin();
|
||||
var fdt = new FdtFileView(pin.Pointer, file.Length);
|
||||
return fdt.MaxTextureIndex;
|
||||
}
|
||||
})));
|
||||
this.prebakedTextureWraps = channelCountsTask.ToImmutableDictionary(
|
||||
x => x.Key,
|
||||
x => x.Value.ContinueWith(y => new IDalamudTextureWrap?[y.Result]));
|
||||
this.texFiles = channelCountsTask.ToImmutableDictionary(
|
||||
x => x.Key,
|
||||
x => x.Value.ContinueWith(
|
||||
y => Enumerable
|
||||
.Range(1, 1 + ((y.Result - 1) / 4))
|
||||
.Select(z => Task.Run(() => dataManager.GetFile<TexFile>(string.Format(x.Key, z))!))
|
||||
.ToArray()));
|
||||
this.defaultGlyphRanges =
|
||||
this.fdtFiles[GameFontFamilyAndSize.Axis12]
|
||||
.ContinueWith(
|
||||
file =>
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
using var pin = file.Result.AsMemory().Pin();
|
||||
var fdt = new FdtFileView(pin.Pointer, file.Result.Length);
|
||||
return fdt.ToGlyphRanges();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to override configuration for UseAxis.
|
||||
/// </summary>
|
||||
public bool? UseAxisOverride { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to use AXIS fonts.
|
||||
/// </summary>
|
||||
public bool UseAxis => this.UseAxisOverride ?? Service<DalamudConfiguration>.Get().UseAxisFontsFromGame;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service instance of <see cref="Framework"/>.
|
||||
/// </summary>
|
||||
public Framework Framework { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the service instance of <see cref="InterfaceManager"/>.<br />
|
||||
/// <see cref="Internal.InterfaceManager.Scene"/> may not yet be available.
|
||||
/// </summary>
|
||||
public InterfaceManager InterfaceManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the async task for <see cref="RawDX11Scene"/> inside <see cref="InterfaceManager"/>.
|
||||
/// </summary>
|
||||
public Task<RawDX11Scene> SceneTask { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default glyph ranges (glyph ranges of <see cref="GameFontFamilyAndSize.Axis12"/>).
|
||||
/// </summary>
|
||||
public ushort[] DefaultGlyphRanges => ExtractResult(this.defaultGlyphRanges);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether game symbol font file is available.
|
||||
/// </summary>
|
||||
public bool HasGameSymbolsFontFile =>
|
||||
this.dalamudAssetManager.IsStreamImmediatelyAvailable(DalamudAsset.LodestoneGameSymbol);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.cancellationTokenSource.Cancel();
|
||||
this.scopedFinalizer.Dispose();
|
||||
this.cancellationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of a class that implements the <see cref="IFontAtlas"/> interface.
|
||||
/// </summary>
|
||||
/// <param name="atlasName">Name of atlas, for debugging and logging purposes.</param>
|
||||
/// <param name="autoRebuildMode">Specify how to auto rebuild.</param>
|
||||
/// <param name="isGlobalScaled">Whether the fonts in the atlas is global scaled.</param>
|
||||
/// <returns>The new font atlas.</returns>
|
||||
public IFontAtlas CreateFontAtlas(
|
||||
string atlasName,
|
||||
FontAtlasAutoRebuildMode autoRebuildMode,
|
||||
bool isGlobalScaled = true) =>
|
||||
new DalamudFontAtlas(this, atlasName, autoRebuildMode, isGlobalScaled);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the font from Dalamud Assets.
|
||||
/// </summary>
|
||||
/// <param name="toolkitPreBuild">The toolkitPostBuild.</param>
|
||||
/// <param name="asset">The font.</param>
|
||||
/// <param name="fontConfig">The font config.</param>
|
||||
/// <returns>The address and size.</returns>
|
||||
public ImFontPtr AddFont(
|
||||
IFontAtlasBuildToolkitPreBuild toolkitPreBuild,
|
||||
DalamudAsset asset,
|
||||
in SafeFontConfig fontConfig) =>
|
||||
toolkitPreBuild.AddFontFromStream(
|
||||
this.dalamudAssetManager.CreateStream(asset),
|
||||
fontConfig,
|
||||
false,
|
||||
$"Asset({asset})");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="FdtReader"/> for the <see cref="GameFontFamilyAndSize"/>.
|
||||
/// </summary>
|
||||
/// <param name="gffas">The font family and size.</param>
|
||||
/// <returns>The <see cref="FdtReader"/>.</returns>
|
||||
public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas]));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView)
|
||||
{
|
||||
var arr = ExtractResult(this.fdtFiles[gffas]);
|
||||
var handle = arr.AsMemory().Pin();
|
||||
try
|
||||
{
|
||||
fdtFileView = new(handle.Pointer, arr.Length);
|
||||
return handle;
|
||||
}
|
||||
catch
|
||||
{
|
||||
handle.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int GetFontTextureCount(string texPathFormat) =>
|
||||
ExtractResult(this.prebakedTextureWraps[texPathFormat]).Length;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public TexFile GetTexFile(string texPathFormat, int index) =>
|
||||
ExtractResult(ExtractResult(this.texFiles[texPathFormat])[index]);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex)
|
||||
{
|
||||
lock (this.prebakedTextureWraps[texPathFormat])
|
||||
{
|
||||
var wraps = ExtractResult(this.prebakedTextureWraps[texPathFormat]);
|
||||
var fileIndex = textureIndex / 4;
|
||||
var channelIndex = FdtReader.FontTableEntry.TextureChannelOrder[textureIndex % 4];
|
||||
wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex);
|
||||
return CloneTextureWrap(wraps[textureIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
private static T ExtractResult<T>(Task<T> t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult();
|
||||
|
||||
private static unsafe void ExtractChannelFromB8G8R8A8(
|
||||
Span<byte> target,
|
||||
ReadOnlySpan<byte> source,
|
||||
int channelIndex,
|
||||
bool targetIsB4G4R4A4)
|
||||
{
|
||||
var numPixels = Math.Min(source.Length / 4, target.Length / (targetIsB4G4R4A4 ? 2 : 4));
|
||||
|
||||
fixed (byte* sourcePtrImmutable = source)
|
||||
{
|
||||
var rptr = sourcePtrImmutable + channelIndex;
|
||||
fixed (void* targetPtr = target)
|
||||
{
|
||||
if (targetIsB4G4R4A4)
|
||||
{
|
||||
var wptr = (ushort*)targetPtr;
|
||||
while (numPixels-- > 0)
|
||||
{
|
||||
*wptr = (ushort)((*rptr << 8) | 0x0FFF);
|
||||
wptr++;
|
||||
rptr += 4;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var wptr = (uint*)targetPtr;
|
||||
while (numPixels-- > 0)
|
||||
{
|
||||
*wptr = (uint)((*rptr << 24) | 0x00FFFFFF);
|
||||
wptr++;
|
||||
rptr += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones a texture wrap, by getting a new reference to the underlying <see cref="ShaderResourceView"/> and the
|
||||
/// texture behind.
|
||||
/// </summary>
|
||||
/// <param name="wrap">The <see cref="IDalamudTextureWrap"/> to clone from.</param>
|
||||
/// <returns>The cloned <see cref="IDalamudTextureWrap"/>.</returns>
|
||||
private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap)
|
||||
{
|
||||
var srv = CppObject.FromPointer<ShaderResourceView>(wrap.ImGuiHandle);
|
||||
using var res = srv.Resource;
|
||||
using var tex2D = res.QueryInterface<Texture2D>();
|
||||
var description = tex2D.Description;
|
||||
return new DalamudTextureWrap(
|
||||
new D3DTextureWrap(
|
||||
srv.QueryInterface<ShaderResourceView>(),
|
||||
description.Width,
|
||||
description.Height));
|
||||
}
|
||||
|
||||
private static unsafe void ExtractChannelFromB4G4R4A4(
|
||||
Span<byte> target,
|
||||
ReadOnlySpan<byte> source,
|
||||
int channelIndex,
|
||||
bool targetIsB4G4R4A4)
|
||||
{
|
||||
var numPixels = Math.Min(source.Length / 2, target.Length / (targetIsB4G4R4A4 ? 2 : 4));
|
||||
fixed (byte* sourcePtrImmutable = source)
|
||||
{
|
||||
var rptr = sourcePtrImmutable + (channelIndex / 2);
|
||||
var rshift = (channelIndex & 1) == 0 ? 0 : 4;
|
||||
fixed (void* targetPtr = target)
|
||||
{
|
||||
if (targetIsB4G4R4A4)
|
||||
{
|
||||
var wptr = (ushort*)targetPtr;
|
||||
while (numPixels-- > 0)
|
||||
{
|
||||
*wptr = (ushort)(((*rptr >> rshift) << 12) | 0x0FFF);
|
||||
wptr++;
|
||||
rptr += 2;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var wptr = (uint*)targetPtr;
|
||||
while (numPixels-- > 0)
|
||||
{
|
||||
var v = (*rptr >> rshift) & 0xF;
|
||||
v |= v << 4;
|
||||
*wptr = (uint)((v << 24) | 0x00FFFFFF);
|
||||
wptr++;
|
||||
rptr += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex)
|
||||
{
|
||||
var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]);
|
||||
var numPixels = texFile.Header.Width * texFile.Header.Height;
|
||||
|
||||
_ = Service<InterfaceManager.InterfaceManagerWithScene>.Get();
|
||||
var targetIsB4G4R4A4 = this.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm);
|
||||
var bpp = targetIsB4G4R4A4 ? 2 : 4;
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(numPixels * bpp);
|
||||
try
|
||||
{
|
||||
var sliceSpan = texFile.SliceSpan(0, 0, out _, out _, out _);
|
||||
switch (texFile.Header.Format)
|
||||
{
|
||||
case TexFile.TextureFormat.B4G4R4A4:
|
||||
// Game ships with this format.
|
||||
ExtractChannelFromB4G4R4A4(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4);
|
||||
break;
|
||||
case TexFile.TextureFormat.B8G8R8A8:
|
||||
// In case of modded font textures.
|
||||
ExtractChannelFromB8G8R8A8(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4);
|
||||
break;
|
||||
default:
|
||||
// Unlikely.
|
||||
ExtractChannelFromB8G8R8A8(buffer, texFile.ImageData, channelIndex, targetIsB4G4R4A4);
|
||||
break;
|
||||
}
|
||||
|
||||
return this.scopedFinalizer.Add(
|
||||
this.InterfaceManager.LoadImageFromDxgiFormat(
|
||||
buffer,
|
||||
texFile.Header.Width * bpp,
|
||||
texFile.Header.Width,
|
||||
texFile.Header.Height,
|
||||
targetIsB4G4R4A4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm));
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
295
Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
Normal file
295
Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin.Internal;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Default implementation for <see cref="FontHandle"/>.
|
||||
/// </summary>
|
||||
internal abstract class FontHandle : IFontHandle
|
||||
{
|
||||
private const int NonMainThreadFontAccessWarningCheckInterval = 10000;
|
||||
private static readonly ConditionalWeakTable<LocalPlugin, object> NonMainThreadFontAccessWarning = new();
|
||||
private static long nextNonMainThreadFontAccessWarningCheck;
|
||||
|
||||
private readonly InterfaceManager interfaceManager;
|
||||
private readonly List<IDisposable> pushedFonts = new(8);
|
||||
|
||||
private IFontHandleManager? manager;
|
||||
private long lastCumulativePresentCalls;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FontHandle"/> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">An instance of <see cref="IFontHandleManager"/>.</param>
|
||||
protected FontHandle(IFontHandleManager manager)
|
||||
{
|
||||
this.interfaceManager = Service<InterfaceManager>.Get();
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event IFontHandle.ImFontChangedDelegate? ImFontChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Event to be called on the first <see cref="IDisposable.Dispose"/> call.
|
||||
/// </summary>
|
||||
protected event Action? Disposed;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Exception? LoadException => this.Manager.Substance?.GetBuildException(this);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Available => (this.Manager.Substance?.GetFontPtr(this) ?? default).IsNotNullAndLoaded();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated <see cref="IFontHandleManager"/>.
|
||||
/// </summary>
|
||||
/// <exception cref="ObjectDisposedException">When the object has already been disposed.</exception>
|
||||
protected IFontHandleManager Manager => this.manager ?? throw new ObjectDisposedException(this.GetType().Name);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.manager is null)
|
||||
return;
|
||||
|
||||
this.Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes <see cref="IFontHandle.ImFontChanged"/>.
|
||||
/// </summary>
|
||||
/// <param name="font">The font, locked during the call of <see cref="ImFontChanged"/>.</param>
|
||||
public void InvokeImFontChanged(ILockedImFont font)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.ImFontChanged?.Invoke(this, font);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, $"{nameof(this.InvokeImFontChanged)}: error");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains an instance of <see cref="ImFontPtr"/> corresponding to this font handle,
|
||||
/// to be released after rendering the current frame.
|
||||
/// </summary>
|
||||
/// <returns>The font pointer, or default if unavailble.</returns>
|
||||
/// <remarks>
|
||||
/// Behavior is undefined on access outside the main thread.
|
||||
/// </remarks>
|
||||
public ImFontPtr LockUntilPostFrame()
|
||||
{
|
||||
if (this.TryLock(out _) is not { } locked)
|
||||
return default;
|
||||
|
||||
if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64)
|
||||
{
|
||||
nextNonMainThreadFontAccessWarningCheck =
|
||||
Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval;
|
||||
var stack = new StackTrace();
|
||||
if (Service<PluginManager>.GetNullable()?.FindCallingPlugin(stack) is { } plugin)
|
||||
{
|
||||
if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _))
|
||||
{
|
||||
NonMainThreadFontAccessWarning.Add(plugin, new());
|
||||
Log.Warning(
|
||||
"[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}",
|
||||
plugin.Name,
|
||||
stack);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dalamud internal should be made safe right now
|
||||
throw new InvalidOperationException("Attempted to access fonts outside the main thread.");
|
||||
}
|
||||
}
|
||||
|
||||
this.interfaceManager.EnqueueDeferredDispose(locked);
|
||||
return locked.ImFont;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to lock the fully constructed instance of <see cref="ImFontPtr"/> corresponding to the this
|
||||
/// <see cref="IFontHandle"/>, for use in any thread.<br />
|
||||
/// Modification of the font will exhibit undefined behavior if some other thread also uses the font.
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">The error message, if any.</param>
|
||||
/// <returns>
|
||||
/// An instance of <see cref="ILockedImFont"/> that <b>must</b> be disposed after use on success;
|
||||
/// <c>null</c> with <paramref name="errorMessage"/> populated on failure.
|
||||
/// </returns>
|
||||
/// <exception cref="ObjectDisposedException">Still may be thrown.</exception>
|
||||
public ILockedImFont? TryLock(out string? errorMessage)
|
||||
{
|
||||
IFontHandleSubstance? prevSubstance = default;
|
||||
while (true)
|
||||
{
|
||||
var substance = this.Manager.Substance;
|
||||
|
||||
// Does the associated IFontAtlas have a built substance?
|
||||
if (substance is null)
|
||||
{
|
||||
errorMessage = "The font atlas has not been built yet.";
|
||||
return null;
|
||||
}
|
||||
|
||||
// Did we loop (because it did not have the requested font),
|
||||
// and are the fetched substance same between loops?
|
||||
if (substance == prevSubstance)
|
||||
{
|
||||
errorMessage = "The font atlas did not built the requested handle yet.";
|
||||
return null;
|
||||
}
|
||||
|
||||
prevSubstance = substance;
|
||||
|
||||
// Try to lock the substance.
|
||||
try
|
||||
{
|
||||
substance.DataRoot.AddRef();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// If it got invalidated, it's probably because a new substance is incoming. Try again.
|
||||
continue;
|
||||
}
|
||||
|
||||
var fontPtr = substance.GetFontPtr(this);
|
||||
if (fontPtr.IsNull())
|
||||
{
|
||||
// The font for the requested handle is unavailable. Release the reference and try again.
|
||||
substance.DataRoot.Release();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Transfer the ownership of reference.
|
||||
errorMessage = null;
|
||||
return new LockedImFont(fontPtr, substance.DataRoot);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILockedImFont Lock() =>
|
||||
this.TryLock(out var errorMessage) ?? throw new InvalidOperationException(errorMessage);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IDisposable Push()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
|
||||
// Warn if the client is not properly managing the pushed font stack.
|
||||
var cumulativePresentCalls = this.interfaceManager.CumulativePresentCalls;
|
||||
if (this.lastCumulativePresentCalls != cumulativePresentCalls)
|
||||
{
|
||||
this.lastCumulativePresentCalls = cumulativePresentCalls;
|
||||
if (this.pushedFonts.Count > 0)
|
||||
{
|
||||
Log.Warning(
|
||||
$"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " +
|
||||
$"You might be missing a call to {nameof(this.Pop)}.");
|
||||
this.pushedFonts.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
var font = default(ImFontPtr);
|
||||
if (this.TryLock(out _) is { } locked)
|
||||
{
|
||||
font = locked.ImFont;
|
||||
this.interfaceManager.EnqueueDeferredDispose(locked);
|
||||
}
|
||||
|
||||
var rented = SimplePushedFont.Rent(this.pushedFonts, font);
|
||||
this.pushedFonts.Add(rented);
|
||||
return rented;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Pop()
|
||||
{
|
||||
ThreadSafety.AssertMainThread();
|
||||
this.pushedFonts[^1].Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<IFontHandle> WaitAsync()
|
||||
{
|
||||
if (this.Available)
|
||||
return Task.FromResult<IFontHandle>(this);
|
||||
|
||||
var tcs = new TaskCompletionSource<IFontHandle>();
|
||||
this.ImFontChanged += OnImFontChanged;
|
||||
this.Disposed += OnDisposed;
|
||||
if (this.Available)
|
||||
OnImFontChanged(this, null);
|
||||
return tcs.Task;
|
||||
|
||||
void OnImFontChanged(IFontHandle unused, ILockedImFont? unused2)
|
||||
{
|
||||
if (tcs.Task.IsCompletedSuccessfully)
|
||||
return;
|
||||
|
||||
this.ImFontChanged -= OnImFontChanged;
|
||||
this.Disposed -= OnDisposed;
|
||||
try
|
||||
{
|
||||
tcs.SetResult(this);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
void OnDisposed()
|
||||
{
|
||||
if (tcs.Task.IsCompletedSuccessfully)
|
||||
return;
|
||||
|
||||
this.ImFontChanged -= OnImFontChanged;
|
||||
this.Disposed -= OnDisposed;
|
||||
try
|
||||
{
|
||||
tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle)));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implementation for <see cref="IDisposable.Dispose"/>.
|
||||
/// </summary>
|
||||
/// <param name="disposing">If <c>true</c>, then the function is being called from <see cref="IDisposable.Dispose"/>.</param>
|
||||
protected void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (this.pushedFonts.Count > 0)
|
||||
Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack.");
|
||||
this.Manager.FreeFontHandle(this);
|
||||
this.manager = null;
|
||||
this.Disposed?.InvokeSafely();
|
||||
this.ImFontChanged = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,877 @@
|
|||
using System.Buffers;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reactive.Disposables;
|
||||
|
||||
using Dalamud.Game.Text;
|
||||
using Dalamud.Interface.GameFonts;
|
||||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Lumina.Data.Files;
|
||||
|
||||
using Vector4 = System.Numerics.Vector4;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// A font handle that uses the game's built-in fonts, optionally with some styling.
|
||||
/// </summary>
|
||||
internal class GamePrebakedFontHandle : FontHandle
|
||||
{
|
||||
/// <summary>
|
||||
/// The smallest value of <see cref="SeIconChar"/>.
|
||||
/// </summary>
|
||||
public static readonly char SeIconCharMin = (char)Enum.GetValues<SeIconChar>().Min();
|
||||
|
||||
/// <summary>
|
||||
/// The largest value of <see cref="SeIconChar"/>.
|
||||
/// </summary>
|
||||
public static readonly char SeIconCharMax = (char)Enum.GetValues<SeIconChar>().Max();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GamePrebakedFontHandle"/> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">An instance of <see cref="IFontHandleManager"/>.</param>
|
||||
/// <param name="style">Font to use.</param>
|
||||
public GamePrebakedFontHandle(IFontHandleManager manager, GameFontStyle style)
|
||||
: base(manager)
|
||||
{
|
||||
if (!Enum.IsDefined(style.FamilyAndSize) || style.FamilyAndSize == GameFontFamilyAndSize.Undefined)
|
||||
throw new ArgumentOutOfRangeException(nameof(style), style, null);
|
||||
|
||||
if (style.SizePt <= 0)
|
||||
throw new ArgumentException($"{nameof(style.SizePt)} must be a positive number.", nameof(style));
|
||||
|
||||
this.FontStyle = style;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provider for <see cref="IDalamudTextureWrap"/> for `common/font/fontNN.tex`.
|
||||
/// </summary>
|
||||
public interface IGameFontTextureProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the <see cref="FdtFileView"/> for the <see cref="GameFontFamilyAndSize"/>.<br />
|
||||
/// <strong>Dispose after use.</strong>
|
||||
/// </summary>
|
||||
/// <param name="gffas">The font family and size.</param>
|
||||
/// <param name="fdtFileView">The view.</param>
|
||||
/// <returns>Dispose this after use..</returns>
|
||||
public MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of font textures.
|
||||
/// </summary>
|
||||
/// <param name="texPathFormat">Format of .tex path.</param>
|
||||
/// <returns>The number of textures.</returns>
|
||||
public int GetFontTextureCount(string texPathFormat);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="TexFile"/> for the given index of a font.
|
||||
/// </summary>
|
||||
/// <param name="texPathFormat">Format of .tex path.</param>
|
||||
/// <param name="index">The index of .tex file.</param>
|
||||
/// <returns>The <see cref="TexFile"/>.</returns>
|
||||
public TexFile GetTexFile(string texPathFormat, int index);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a new reference of the font texture.
|
||||
/// </summary>
|
||||
/// <param name="texPathFormat">Format of .tex path.</param>
|
||||
/// <param name="textureIndex">Texture index.</param>
|
||||
/// <returns>The texture.</returns>
|
||||
public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font style.
|
||||
/// </summary>
|
||||
public GameFontStyle FontStyle { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString() => $"{nameof(GamePrebakedFontHandle)}({this.FontStyle})";
|
||||
|
||||
/// <summary>
|
||||
/// Manager for <see cref="GamePrebakedFontHandle"/>s.
|
||||
/// </summary>
|
||||
internal sealed class HandleManager : IFontHandleManager
|
||||
{
|
||||
private readonly Dictionary<GameFontStyle, int> gameFontsRc = new();
|
||||
private readonly HashSet<GamePrebakedFontHandle> handles = new();
|
||||
private readonly object syncRoot = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HandleManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="atlasName">The name of the owner atlas.</param>
|
||||
/// <param name="gameFontTextureProvider">An instance of <see cref="IGameFontTextureProvider"/>.</param>
|
||||
public HandleManager(string atlasName, IGameFontTextureProvider gameFontTextureProvider)
|
||||
{
|
||||
this.GameFontTextureProvider = gameFontTextureProvider;
|
||||
this.Name = $"{atlasName}:{nameof(GamePrebakedFontHandle)}:Manager";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event Action? RebuildRecommend;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string Name { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFontHandleSubstance? Substance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets an instance of <see cref="IGameFontTextureProvider"/>.
|
||||
/// </summary>
|
||||
public IGameFontTextureProvider GameFontTextureProvider { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
// empty
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IFontAtlas.NewGameFontHandle"/>
|
||||
public IFontHandle NewFontHandle(GameFontStyle style)
|
||||
{
|
||||
var handle = new GamePrebakedFontHandle(this, style);
|
||||
bool suggestRebuild;
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
this.handles.Add(handle);
|
||||
this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1;
|
||||
suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true;
|
||||
}
|
||||
|
||||
if (suggestRebuild)
|
||||
this.RebuildRecommend?.Invoke();
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void FreeFontHandle(IFontHandle handle)
|
||||
{
|
||||
if (handle is not GamePrebakedFontHandle ggfh)
|
||||
return;
|
||||
|
||||
lock (this.syncRoot)
|
||||
{
|
||||
this.handles.Remove(ggfh);
|
||||
if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle))
|
||||
return;
|
||||
|
||||
if ((this.gameFontsRc[ggfh.FontStyle] -= 1) == 0)
|
||||
this.gameFontsRc.Remove(ggfh.FontStyle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFontHandleSubstance NewSubstance(IRefCountable dataRoot)
|
||||
{
|
||||
lock (this.syncRoot)
|
||||
return new HandleSubstance(this, dataRoot, this.handles.ToArray(), this.gameFontsRc.Keys);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Substance from <see cref="HandleManager"/>.
|
||||
/// </summary>
|
||||
internal sealed class HandleSubstance : IFontHandleSubstance
|
||||
{
|
||||
private readonly HandleManager handleManager;
|
||||
private readonly HashSet<GameFontStyle> gameFontStyles;
|
||||
|
||||
// Owned by this class, but ImFontPtr values still do not belong to this.
|
||||
private readonly Dictionary<GameFontStyle, FontDrawPlan> fonts = new();
|
||||
private readonly Dictionary<GameFontStyle, Exception?> buildExceptions = new();
|
||||
private readonly List<(ImFontPtr Font, GameFontStyle Style, ushort[]? Ranges)> attachments = new();
|
||||
|
||||
private readonly HashSet<ImFontPtr> templatedFonts = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HandleSubstance"/> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">The manager.</param>
|
||||
/// <param name="dataRoot">The data root.</param>
|
||||
/// <param name="relevantHandles">The relevant handles.</param>
|
||||
/// <param name="gameFontStyles">The game font styles.</param>
|
||||
public HandleSubstance(
|
||||
HandleManager manager,
|
||||
IRefCountable dataRoot,
|
||||
GamePrebakedFontHandle[] relevantHandles,
|
||||
IEnumerable<GameFontStyle> gameFontStyles)
|
||||
{
|
||||
// We do not call dataRoot.AddRef; this object is dependant on lifetime of dataRoot.
|
||||
|
||||
this.handleManager = manager;
|
||||
this.DataRoot = dataRoot;
|
||||
this.RelevantHandles = relevantHandles;
|
||||
this.gameFontStyles = new(gameFontStyles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the relevant handles.
|
||||
/// </summary>
|
||||
// Not owned by this class. Do not dispose.
|
||||
public GamePrebakedFontHandle[] RelevantHandles { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
ICollection<FontHandle> IFontHandleSubstance.RelevantHandles => this.RelevantHandles;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IRefCountable DataRoot { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IFontHandleManager Manager => this.handleManager;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public IFontAtlasBuildToolkitPreBuild? PreBuildToolkitForApi9Compat { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public bool CreateFontOnAccess { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
// empty
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches game symbols to the given font. If font is null, it will be created.
|
||||
/// </summary>
|
||||
/// <param name="toolkitPreBuild">The toolkitPostBuild.</param>
|
||||
/// <param name="font">The font to attach to.</param>
|
||||
/// <param name="style">The game font style.</param>
|
||||
/// <param name="glyphRanges">The intended glyph ranges.</param>
|
||||
/// <returns><paramref name="font"/> if it is not empty; otherwise a new font.</returns>
|
||||
public ImFontPtr AttachGameGlyphs(
|
||||
IFontAtlasBuildToolkitPreBuild toolkitPreBuild,
|
||||
ImFontPtr font,
|
||||
GameFontStyle style,
|
||||
ushort[]? glyphRanges = null)
|
||||
{
|
||||
if (font.IsNull())
|
||||
font = this.CreateTemplateFont(toolkitPreBuild, style.SizePx);
|
||||
this.attachments.Add((font, style, glyphRanges));
|
||||
return font;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or gets a relevant <see cref="ImFontPtr"/> for the given <see cref="GameFontStyle"/>.
|
||||
/// </summary>
|
||||
/// <param name="style">The game font style.</param>
|
||||
/// <param name="toolkitPreBuild">The toolkitPostBuild.</param>
|
||||
/// <returns>The font.</returns>
|
||||
public ImFontPtr GetOrCreateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!this.fonts.TryGetValue(style, out var plan))
|
||||
{
|
||||
plan = new(
|
||||
style,
|
||||
toolkitPreBuild.Scale,
|
||||
this.handleManager.GameFontTextureProvider,
|
||||
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
|
||||
this.fonts[style] = plan;
|
||||
}
|
||||
|
||||
plan.AttachFont(plan.FullRangeFont);
|
||||
return plan.FullRangeFont;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.buildExceptions[style] = e;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Use this on API 10.
|
||||
// /// <inheritdoc/>
|
||||
// public ImFontPtr GetFontPtr(IFontHandle handle) =>
|
||||
// handle is GamePrebakedFontHandle ggfh
|
||||
// ? this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont ?? default
|
||||
// : default;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public ImFontPtr GetFontPtr(IFontHandle handle)
|
||||
{
|
||||
if (handle is not GamePrebakedFontHandle ggfh)
|
||||
return default;
|
||||
if (this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont is { } font)
|
||||
return font;
|
||||
if (!this.CreateFontOnAccess)
|
||||
return default;
|
||||
if (this.PreBuildToolkitForApi9Compat is not { } tk)
|
||||
return default;
|
||||
return this.GetOrCreateFont(ggfh.FontStyle, tk);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Exception? GetBuildException(IFontHandle handle) =>
|
||||
handle is GamePrebakedFontHandle ggfh ? this.buildExceptions.GetValueOrDefault(ggfh.FontStyle) : default;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
|
||||
{
|
||||
foreach (var style in this.gameFontStyles)
|
||||
{
|
||||
if (this.fonts.ContainsKey(style))
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
_ = this.GetOrCreateFont(style, toolkitPreBuild);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore; it should have been recorded from the call
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild)
|
||||
{
|
||||
foreach (var (font, style, ranges) in this.attachments)
|
||||
{
|
||||
var effectiveStyle =
|
||||
toolkitPreBuild.IsGlobalScaleIgnored(font)
|
||||
? style.Scale(1 / toolkitPreBuild.Scale)
|
||||
: style;
|
||||
if (!this.fonts.TryGetValue(style, out var plan))
|
||||
{
|
||||
plan = new(
|
||||
effectiveStyle,
|
||||
toolkitPreBuild.Scale,
|
||||
this.handleManager.GameFontTextureProvider,
|
||||
this.CreateTemplateFont(toolkitPreBuild, style.SizePx));
|
||||
this.fonts[style] = plan;
|
||||
}
|
||||
|
||||
plan.AttachFont(font, ranges);
|
||||
}
|
||||
|
||||
foreach (var plan in this.fonts.Values)
|
||||
{
|
||||
plan.EnsureGlyphs(toolkitPreBuild.NewImAtlas);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public unsafe void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
|
||||
{
|
||||
var allTextureIndices = new Dictionary<string, int[]>();
|
||||
var allTexFiles = new Dictionary<string, TexFile[]>();
|
||||
using var rentReturn = Disposable.Create(
|
||||
() =>
|
||||
{
|
||||
foreach (var x in allTextureIndices.Values)
|
||||
ArrayPool<int>.Shared.Return(x);
|
||||
foreach (var x in allTexFiles.Values)
|
||||
ArrayPool<TexFile>.Shared.Return(x);
|
||||
});
|
||||
|
||||
var pixels8Array = new byte*[toolkitPostBuild.NewImAtlas.Textures.Size];
|
||||
var widths = new int[toolkitPostBuild.NewImAtlas.Textures.Size];
|
||||
for (var i = 0; i < pixels8Array.Length; i++)
|
||||
toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out _);
|
||||
|
||||
foreach (var (style, plan) in this.fonts)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var font in plan.Ranges.Keys)
|
||||
this.PatchFontMetricsIfNecessary(style, font, toolkitPostBuild.Scale);
|
||||
|
||||
plan.SetFullRangeFontGlyphs(toolkitPostBuild, allTexFiles, allTextureIndices, pixels8Array, widths);
|
||||
plan.CopyGlyphsToRanges(toolkitPostBuild);
|
||||
plan.PostProcessFullRangeFont(toolkitPostBuild.Scale);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this.buildExceptions[style] = e;
|
||||
this.fonts[style] = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new template font.
|
||||
/// </summary>
|
||||
/// <param name="toolkitPreBuild">The toolkitPostBuild.</param>
|
||||
/// <param name="sizePx">The size of the font.</param>
|
||||
/// <returns>The font.</returns>
|
||||
private ImFontPtr CreateTemplateFont(IFontAtlasBuildToolkitPreBuild toolkitPreBuild, float sizePx)
|
||||
{
|
||||
var font = toolkitPreBuild.AddDalamudAssetFont(
|
||||
DalamudAsset.NotoSansJpMedium,
|
||||
new()
|
||||
{
|
||||
GlyphRanges = new ushort[] { ' ', ' ', '\0' },
|
||||
SizePx = sizePx,
|
||||
});
|
||||
this.templatedFonts.Add(font);
|
||||
return font;
|
||||
}
|
||||
|
||||
private unsafe void PatchFontMetricsIfNecessary(GameFontStyle style, ImFontPtr font, float atlasScale)
|
||||
{
|
||||
if (!this.templatedFonts.Contains(font))
|
||||
return;
|
||||
|
||||
var fas = style.Scale(atlasScale).FamilyAndSize;
|
||||
using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt);
|
||||
ref var fdtFontHeader = ref fdt.FontHeader;
|
||||
var fontPtr = font.NativePtr;
|
||||
|
||||
var scale = style.SizePt / fdtFontHeader.Size;
|
||||
fontPtr->Ascent = fdtFontHeader.Ascent * scale;
|
||||
fontPtr->Descent = fdtFontHeader.Descent * scale;
|
||||
fontPtr->EllipsisChar = '…';
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.MaintainabilityRules",
|
||||
"SA1401:Fields should be private",
|
||||
Justification = "Internal")]
|
||||
private sealed class FontDrawPlan : IDisposable
|
||||
{
|
||||
public readonly GameFontStyle Style;
|
||||
public readonly GameFontStyle BaseStyle;
|
||||
public readonly GameFontFamilyAndSizeAttribute BaseAttr;
|
||||
public readonly int TexCount;
|
||||
public readonly Dictionary<ImFontPtr, BitArray> Ranges = new();
|
||||
public readonly List<(int RectId, int FdtGlyphIndex)> Rects = new();
|
||||
public readonly ushort[] RectLookup = new ushort[0x10000];
|
||||
public readonly FdtFileView Fdt;
|
||||
public readonly ImFontPtr FullRangeFont;
|
||||
|
||||
private readonly IDisposable fdtHandle;
|
||||
private readonly IGameFontTextureProvider gftp;
|
||||
|
||||
public FontDrawPlan(
|
||||
GameFontStyle style,
|
||||
float scale,
|
||||
IGameFontTextureProvider gameFontTextureProvider,
|
||||
ImFontPtr fullRangeFont)
|
||||
{
|
||||
this.Style = style;
|
||||
this.BaseStyle = style.Scale(scale);
|
||||
this.BaseAttr = this.BaseStyle.FamilyAndSize.GetAttribute<GameFontFamilyAndSizeAttribute>()!;
|
||||
this.gftp = gameFontTextureProvider;
|
||||
this.TexCount = this.gftp.GetFontTextureCount(this.BaseAttr.TexPathFormat);
|
||||
this.fdtHandle = this.gftp.CreateFdtFileView(this.BaseStyle.FamilyAndSize, out this.Fdt);
|
||||
this.RectLookup.AsSpan().Fill(ushort.MaxValue);
|
||||
this.FullRangeFont = fullRangeFont;
|
||||
this.Ranges[fullRangeFont] = new(0x10000);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
this.fdtHandle.Dispose();
|
||||
}
|
||||
|
||||
public void AttachFont(ImFontPtr font, ushort[]? glyphRanges = null)
|
||||
{
|
||||
if (!this.Ranges.TryGetValue(font, out var rangeBitArray))
|
||||
rangeBitArray = this.Ranges[font] = new(0x10000);
|
||||
|
||||
if (glyphRanges is null)
|
||||
{
|
||||
foreach (ref var g in this.Fdt.Glyphs)
|
||||
{
|
||||
var c = g.CharInt;
|
||||
if (c is >= 0x20 and <= 0xFFFE)
|
||||
rangeBitArray[c] = true;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < glyphRanges.Length - 1; i += 2)
|
||||
{
|
||||
if (glyphRanges[i] == 0)
|
||||
break;
|
||||
var from = (int)glyphRanges[i];
|
||||
var to = (int)glyphRanges[i + 1];
|
||||
for (var j = from; j <= to; j++)
|
||||
rangeBitArray[j] = true;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void EnsureGlyphs(ImFontAtlasPtr atlas)
|
||||
{
|
||||
var glyphs = this.Fdt.Glyphs;
|
||||
var ranges = this.Ranges[this.FullRangeFont];
|
||||
foreach (var (font, extraRange) in this.Ranges)
|
||||
{
|
||||
if (font.NativePtr != this.FullRangeFont.NativePtr)
|
||||
ranges.Or(extraRange);
|
||||
}
|
||||
|
||||
if (this.Style is not { Weight: 0, SkewStrength: 0 })
|
||||
{
|
||||
for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++)
|
||||
{
|
||||
ref var glyph = ref glyphs[fdtGlyphIndex];
|
||||
var cint = glyph.CharInt;
|
||||
if (cint > char.MaxValue)
|
||||
continue;
|
||||
if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue)
|
||||
continue;
|
||||
|
||||
var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(this.Fdt.FontHeader, glyph);
|
||||
this.RectLookup[cint] = (ushort)this.Rects.Count;
|
||||
this.Rects.Add(
|
||||
(
|
||||
atlas.AddCustomRectFontGlyph(
|
||||
this.FullRangeFont,
|
||||
(char)cint,
|
||||
glyph.BoundingWidth + widthAdjustment,
|
||||
glyph.BoundingHeight,
|
||||
glyph.AdvanceWidth,
|
||||
new(this.BaseAttr.HorizontalOffset, glyph.CurrentOffsetY)),
|
||||
fdtGlyphIndex));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++)
|
||||
{
|
||||
ref var glyph = ref glyphs[fdtGlyphIndex];
|
||||
var cint = glyph.CharInt;
|
||||
if (cint > char.MaxValue)
|
||||
continue;
|
||||
if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue)
|
||||
continue;
|
||||
|
||||
this.RectLookup[cint] = (ushort)this.Rects.Count;
|
||||
this.Rects.Add((-1, fdtGlyphIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void PostProcessFullRangeFont(float atlasScale)
|
||||
{
|
||||
var round = 1 / atlasScale;
|
||||
var pfrf = this.FullRangeFont.NativePtr;
|
||||
ref var frf = ref *pfrf;
|
||||
|
||||
frf.FontSize = MathF.Round(frf.FontSize / round) * round;
|
||||
frf.Ascent = MathF.Round(frf.Ascent / round) * round;
|
||||
frf.Descent = MathF.Round(frf.Descent / round) * round;
|
||||
|
||||
var scale = this.Style.SizePt / this.Fdt.FontHeader.Size;
|
||||
foreach (ref var g in this.FullRangeFont.GlyphsWrapped().DataSpan)
|
||||
{
|
||||
var w = (g.X1 - g.X0) * scale;
|
||||
var h = (g.Y1 - g.Y0) * scale;
|
||||
g.X0 = MathF.Round((g.X0 * scale) / round) * round;
|
||||
g.Y0 = MathF.Round((g.Y0 * scale) / round) * round;
|
||||
g.X1 = g.X0 + w;
|
||||
g.Y1 = g.Y0 + h;
|
||||
g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round;
|
||||
}
|
||||
|
||||
var fullRange = this.Ranges[this.FullRangeFont];
|
||||
foreach (ref var k in this.Fdt.PairAdjustments)
|
||||
{
|
||||
var (leftInt, rightInt) = (k.LeftInt, k.RightInt);
|
||||
if (leftInt > char.MaxValue || rightInt > char.MaxValue)
|
||||
continue;
|
||||
if (!fullRange[leftInt] || !fullRange[rightInt])
|
||||
continue;
|
||||
ImGuiNative.ImFont_AddKerningPair(
|
||||
pfrf,
|
||||
(ushort)leftInt,
|
||||
(ushort)rightInt,
|
||||
MathF.Round((k.RightOffset * scale) / round) * round);
|
||||
}
|
||||
|
||||
pfrf->FallbackGlyph = null;
|
||||
ImGuiNative.ImFont_BuildLookupTable(pfrf);
|
||||
|
||||
foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints)
|
||||
{
|
||||
var glyph = ImGuiNative.ImFont_FindGlyphNoFallback(pfrf, fallbackCharCandidate);
|
||||
if ((nint)glyph == IntPtr.Zero)
|
||||
continue;
|
||||
frf.FallbackChar = fallbackCharCandidate;
|
||||
frf.FallbackGlyph = glyph;
|
||||
frf.FallbackHotData =
|
||||
(ImFontGlyphHotData*)frf.IndexedHotData.Address<ImGuiHelpers.ImFontGlyphHotDataReal>(
|
||||
fallbackCharCandidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild)
|
||||
{
|
||||
var scale = this.Style.SizePt / this.Fdt.FontHeader.Size;
|
||||
var atlasScale = toolkitPostBuild.Scale;
|
||||
var round = 1 / atlasScale;
|
||||
|
||||
foreach (var (font, rangeBits) in this.Ranges)
|
||||
{
|
||||
if (font.NativePtr == this.FullRangeFont.NativePtr)
|
||||
continue;
|
||||
|
||||
var noGlobalScale = toolkitPostBuild.IsGlobalScaleIgnored(font);
|
||||
|
||||
var lookup = font.IndexLookupWrapped();
|
||||
var glyphs = font.GlyphsWrapped();
|
||||
foreach (ref var sourceGlyph in this.FullRangeFont.GlyphsWrapped().DataSpan)
|
||||
{
|
||||
if (!rangeBits[sourceGlyph.Codepoint])
|
||||
continue;
|
||||
|
||||
var glyphIndex = ushort.MaxValue;
|
||||
if (sourceGlyph.Codepoint < lookup.Length)
|
||||
glyphIndex = lookup[sourceGlyph.Codepoint];
|
||||
|
||||
if (glyphIndex == ushort.MaxValue)
|
||||
{
|
||||
glyphIndex = (ushort)glyphs.Length;
|
||||
glyphs.Add(default);
|
||||
}
|
||||
|
||||
ref var g = ref glyphs[glyphIndex];
|
||||
g = sourceGlyph;
|
||||
if (noGlobalScale)
|
||||
{
|
||||
g.XY *= scale;
|
||||
g.AdvanceX *= scale;
|
||||
}
|
||||
else
|
||||
{
|
||||
var w = (g.X1 - g.X0) * scale;
|
||||
var h = (g.Y1 - g.Y0) * scale;
|
||||
g.X0 = MathF.Round((g.X0 * scale) / round) * round;
|
||||
g.Y0 = MathF.Round((g.Y0 * scale) / round) * round;
|
||||
g.X1 = g.X0 + w;
|
||||
g.Y1 = g.Y0 + h;
|
||||
g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ref var k in this.Fdt.PairAdjustments)
|
||||
{
|
||||
var (leftInt, rightInt) = (k.LeftInt, k.RightInt);
|
||||
if (leftInt > char.MaxValue || rightInt > char.MaxValue)
|
||||
continue;
|
||||
if (!rangeBits[leftInt] || !rangeBits[rightInt])
|
||||
continue;
|
||||
if (noGlobalScale)
|
||||
{
|
||||
font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
font.AddKerningPair(
|
||||
(ushort)leftInt,
|
||||
(ushort)rightInt,
|
||||
MathF.Round((k.RightOffset * scale) / round) * round);
|
||||
}
|
||||
}
|
||||
|
||||
font.NativePtr->FallbackGlyph = null;
|
||||
font.BuildLookupTable();
|
||||
|
||||
foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints)
|
||||
{
|
||||
var glyph = font.FindGlyphNoFallback(fallbackCharCandidate).NativePtr;
|
||||
if ((nint)glyph == IntPtr.Zero)
|
||||
continue;
|
||||
|
||||
ref var frf = ref *font.NativePtr;
|
||||
frf.FallbackChar = fallbackCharCandidate;
|
||||
frf.FallbackGlyph = glyph;
|
||||
frf.FallbackHotData =
|
||||
(ImFontGlyphHotData*)frf.IndexedHotData.Address<ImGuiHelpers.ImFontGlyphHotDataReal>(
|
||||
fallbackCharCandidate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public unsafe void SetFullRangeFontGlyphs(
|
||||
IFontAtlasBuildToolkitPostBuild toolkitPostBuild,
|
||||
Dictionary<string, TexFile[]> allTexFiles,
|
||||
Dictionary<string, int[]> allTextureIndices,
|
||||
byte*[] pixels8Array,
|
||||
int[] widths)
|
||||
{
|
||||
var glyphs = this.FullRangeFont.GlyphsWrapped();
|
||||
var lookups = this.FullRangeFont.IndexLookupWrapped();
|
||||
|
||||
ref var fdtFontHeader = ref this.Fdt.FontHeader;
|
||||
var fdtGlyphs = this.Fdt.Glyphs;
|
||||
var fdtTexSize = new Vector4(
|
||||
this.Fdt.FontHeader.TextureWidth,
|
||||
this.Fdt.FontHeader.TextureHeight,
|
||||
this.Fdt.FontHeader.TextureWidth,
|
||||
this.Fdt.FontHeader.TextureHeight);
|
||||
|
||||
if (!allTexFiles.TryGetValue(this.BaseAttr.TexPathFormat, out var texFiles))
|
||||
{
|
||||
allTexFiles.Add(
|
||||
this.BaseAttr.TexPathFormat,
|
||||
texFiles = ArrayPool<TexFile>.Shared.Rent(this.TexCount));
|
||||
}
|
||||
|
||||
if (!allTextureIndices.TryGetValue(this.BaseAttr.TexPathFormat, out var textureIndices))
|
||||
{
|
||||
allTextureIndices.Add(
|
||||
this.BaseAttr.TexPathFormat,
|
||||
textureIndices = ArrayPool<int>.Shared.Rent(this.TexCount));
|
||||
textureIndices.AsSpan(0, this.TexCount).Fill(-1);
|
||||
}
|
||||
|
||||
var pixelWidth = Math.Max(1, (int)MathF.Ceiling(this.BaseStyle.Weight + 1));
|
||||
var pixelStrength = stackalloc byte[pixelWidth];
|
||||
for (var i = 0; i < pixelWidth; i++)
|
||||
pixelStrength[i] = (byte)(255 * Math.Min(1f, (this.BaseStyle.Weight + 1) - i));
|
||||
|
||||
var minGlyphY = 0;
|
||||
var maxGlyphY = 0;
|
||||
foreach (ref var g in fdtGlyphs)
|
||||
{
|
||||
minGlyphY = Math.Min(g.CurrentOffsetY, minGlyphY);
|
||||
maxGlyphY = Math.Max(g.BoundingHeight + g.CurrentOffsetY, maxGlyphY);
|
||||
}
|
||||
|
||||
var horzShift = stackalloc int[maxGlyphY - minGlyphY];
|
||||
var horzBlend = stackalloc byte[maxGlyphY - minGlyphY];
|
||||
horzShift -= minGlyphY;
|
||||
horzBlend -= minGlyphY;
|
||||
if (this.BaseStyle.BaseSkewStrength != 0)
|
||||
{
|
||||
for (var i = minGlyphY; i < maxGlyphY; i++)
|
||||
{
|
||||
float blend = this.BaseStyle.BaseSkewStrength switch
|
||||
{
|
||||
> 0 => fdtFontHeader.LineHeight - i,
|
||||
< 0 => -i,
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
blend *= this.BaseStyle.BaseSkewStrength / fdtFontHeader.LineHeight;
|
||||
horzShift[i] = (int)MathF.Floor(blend);
|
||||
horzBlend[i] = (byte)(255 * (blend - horzShift[i]));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (rectId, fdtGlyphIndex) in this.Rects)
|
||||
{
|
||||
ref var fdtGlyph = ref fdtGlyphs[fdtGlyphIndex];
|
||||
if (rectId == -1)
|
||||
{
|
||||
ref var textureIndex = ref textureIndices[fdtGlyph.TextureIndex];
|
||||
if (textureIndex == -1)
|
||||
{
|
||||
textureIndex = toolkitPostBuild.StoreTexture(
|
||||
this.gftp.NewFontTextureRef(this.BaseAttr.TexPathFormat, fdtGlyph.TextureIndex),
|
||||
true);
|
||||
}
|
||||
|
||||
var glyph = new ImGuiHelpers.ImFontGlyphReal
|
||||
{
|
||||
AdvanceX = fdtGlyph.AdvanceWidth,
|
||||
Codepoint = fdtGlyph.Char,
|
||||
Colored = false,
|
||||
TextureIndex = textureIndex,
|
||||
Visible = true,
|
||||
X0 = this.BaseAttr.HorizontalOffset,
|
||||
Y0 = fdtGlyph.CurrentOffsetY,
|
||||
U0 = fdtGlyph.TextureOffsetX,
|
||||
V0 = fdtGlyph.TextureOffsetY,
|
||||
U1 = fdtGlyph.BoundingWidth,
|
||||
V1 = fdtGlyph.BoundingHeight,
|
||||
};
|
||||
|
||||
glyph.XY1 = glyph.XY0 + glyph.UV1;
|
||||
glyph.UV1 += glyph.UV0;
|
||||
glyph.UV /= fdtTexSize;
|
||||
|
||||
glyphs.Add(glyph);
|
||||
}
|
||||
else
|
||||
{
|
||||
ref var rc = ref *(ImGuiHelpers.ImFontAtlasCustomRectReal*)toolkitPostBuild.NewImAtlas
|
||||
.GetCustomRectByIndex(rectId)
|
||||
.NativePtr;
|
||||
var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(fdtFontHeader, fdtGlyph);
|
||||
|
||||
// Glyph is scaled at this point; undo that.
|
||||
ref var glyph = ref glyphs[lookups[rc.GlyphId]];
|
||||
glyph.X0 = this.BaseAttr.HorizontalOffset;
|
||||
glyph.Y0 = fdtGlyph.CurrentOffsetY;
|
||||
glyph.X1 = glyph.X0 + fdtGlyph.BoundingWidth + widthAdjustment;
|
||||
glyph.Y1 = glyph.Y0 + fdtGlyph.BoundingHeight;
|
||||
glyph.AdvanceX = fdtGlyph.AdvanceWidth;
|
||||
|
||||
var pixels8 = pixels8Array[rc.TextureIndex];
|
||||
var width = widths[rc.TextureIndex];
|
||||
texFiles[fdtGlyph.TextureFileIndex] ??=
|
||||
this.gftp.GetTexFile(this.BaseAttr.TexPathFormat, fdtGlyph.TextureFileIndex);
|
||||
var sourceBuffer = texFiles[fdtGlyph.TextureFileIndex].ImageData;
|
||||
var sourceBufferDelta = fdtGlyph.TextureChannelByteIndex;
|
||||
|
||||
for (var y = 0; y < fdtGlyph.BoundingHeight; y++)
|
||||
{
|
||||
var sourcePixelIndex =
|
||||
((fdtGlyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + fdtGlyph.TextureOffsetX;
|
||||
sourcePixelIndex *= 4;
|
||||
sourcePixelIndex += sourceBufferDelta;
|
||||
var blend1 = horzBlend[fdtGlyph.CurrentOffsetY + y];
|
||||
|
||||
var targetOffset = ((rc.Y + y) * width) + rc.X;
|
||||
for (var x = 0; x < rc.Width; x++)
|
||||
pixels8[targetOffset + x] = 0;
|
||||
|
||||
targetOffset += horzShift[fdtGlyph.CurrentOffsetY + y];
|
||||
if (blend1 == 0)
|
||||
{
|
||||
for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++)
|
||||
{
|
||||
var n = sourceBuffer[sourcePixelIndex + 4];
|
||||
for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++)
|
||||
{
|
||||
ref var p = ref pixels8[targetOffset + boldOffset];
|
||||
p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255));
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var blend2 = 255 - blend1;
|
||||
for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++)
|
||||
{
|
||||
var a1 = sourceBuffer[sourcePixelIndex];
|
||||
var a2 = x == fdtGlyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourcePixelIndex + 4];
|
||||
var n = (a1 * blend1) + (a2 * blend2);
|
||||
|
||||
for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++)
|
||||
{
|
||||
ref var p = ref pixels8[targetOffset + boldOffset];
|
||||
p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255 / 255));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
using Dalamud.Utility;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Manager for <see cref="IFontHandle"/>.
|
||||
/// </summary>
|
||||
internal interface IFontHandleManager : IDisposable
|
||||
{
|
||||
/// <inheritdoc cref="IFontAtlas.RebuildRecommend"/>
|
||||
event Action? RebuildRecommend;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the font handle manager. For logging and debugging purposes.
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the active font handle substance.
|
||||
/// </summary>
|
||||
IFontHandleSubstance? Substance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Decrease font reference counter.
|
||||
/// </summary>
|
||||
/// <param name="handle">Handle being released.</param>
|
||||
void FreeFontHandle(IFontHandle handle);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new substance of the font atlas.
|
||||
/// </summary>
|
||||
/// <param name="dataRoot">The data root.</param>
|
||||
/// <returns>The new substance.</returns>
|
||||
IFontHandleSubstance NewSubstance(IRefCountable dataRoot);
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Substance of a font.
|
||||
/// </summary>
|
||||
internal interface IFontHandleSubstance : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the data root relevant to this instance of <see cref="IFontHandleSubstance"/>.
|
||||
/// </summary>
|
||||
IRefCountable DataRoot { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the manager relevant to this instance of <see cref="IFontHandleSubstance"/>.
|
||||
/// </summary>
|
||||
IFontHandleManager Manager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the relevant <see cref="IFontAtlasBuildToolkitPreBuild"/> for this.
|
||||
/// </summary>
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
IFontAtlasBuildToolkitPreBuild? PreBuildToolkitForApi9Compat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to create a new instance of <see cref="ImGuiNET.ImFontPtr"/> on first
|
||||
/// access, for compatibility with API 9.
|
||||
/// </summary>
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
bool CreateFontOnAccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the relevant handles.
|
||||
/// </summary>
|
||||
public ICollection<FontHandle> RelevantHandles { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the font.
|
||||
/// </summary>
|
||||
/// <param name="handle">The handle to get from.</param>
|
||||
/// <returns>Corresponding font or null.</returns>
|
||||
ImFontPtr GetFontPtr(IFontHandle handle);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the exception happened while loading for the font.
|
||||
/// </summary>
|
||||
/// <param name="handle">The handle to get from.</param>
|
||||
/// <returns>Corresponding font or null.</returns>
|
||||
Exception? GetBuildException(IFontHandle handle);
|
||||
|
||||
/// <summary>
|
||||
/// Called before <see cref="ImFontAtlasPtr.Build"/> call.
|
||||
/// </summary>
|
||||
/// <param name="toolkitPreBuild">The toolkit.</param>
|
||||
void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild);
|
||||
|
||||
/// <summary>
|
||||
/// Called between <see cref="OnPreBuild"/> and <see cref="ImFontAtlasPtr.Build"/> calls.<br />
|
||||
/// Any further modification to <see cref="IFontAtlasBuildToolkit.Fonts"/> will result in undefined behavior.
|
||||
/// </summary>
|
||||
/// <param name="toolkitPreBuild">The toolkit.</param>
|
||||
void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild);
|
||||
|
||||
/// <summary>
|
||||
/// Called after <see cref="ImFontAtlasPtr.Build"/> call.
|
||||
/// </summary>
|
||||
/// <param name="toolkitPostBuild">The toolkit.</param>
|
||||
void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild);
|
||||
}
|
||||
62
Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs
Normal file
62
Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using Dalamud.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// The implementation for <see cref="ILockedImFont"/>.
|
||||
/// </summary>
|
||||
internal class LockedImFont : ILockedImFont
|
||||
{
|
||||
private IRefCountable? owner;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LockedImFont"/> class.
|
||||
/// Ownership of reference of <paramref name="owner"/> is transferred.
|
||||
/// </summary>
|
||||
/// <param name="font">The contained font.</param>
|
||||
/// <param name="owner">The owner.</param>
|
||||
/// <returns>The rented instance of <see cref="LockedImFont"/>.</returns>
|
||||
internal LockedImFont(ImFontPtr font, IRefCountable owner)
|
||||
{
|
||||
this.ImFont = font;
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes an instance of the <see cref="LockedImFont"/> class.
|
||||
/// </summary>
|
||||
~LockedImFont() => this.FreeOwner();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ImFontPtr ImFont { get; private set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ILockedImFont NewRef()
|
||||
{
|
||||
if (this.owner is null)
|
||||
throw new ObjectDisposedException(nameof(LockedImFont));
|
||||
|
||||
var newRef = new LockedImFont(this.ImFont, this.owner);
|
||||
this.owner.AddRef();
|
||||
return newRef;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
this.FreeOwner();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void FreeOwner()
|
||||
{
|
||||
if (this.owner is null)
|
||||
return;
|
||||
|
||||
this.owner.Release();
|
||||
this.owner = null;
|
||||
this.ImFont = default;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
using Dalamud.Interface.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
using Microsoft.Extensions.ObjectPool;
|
||||
|
||||
using Serilog;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Reusable font push/popper.
|
||||
/// </summary>
|
||||
internal sealed class SimplePushedFont : IDisposable
|
||||
{
|
||||
// Using constructor instead of DefaultObjectPoolProvider, since we do not want the pool to call Dispose.
|
||||
private static readonly ObjectPool<SimplePushedFont> Pool =
|
||||
new DefaultObjectPool<SimplePushedFont>(new DefaultPooledObjectPolicy<SimplePushedFont>());
|
||||
|
||||
private List<IDisposable>? stack;
|
||||
private ImFontPtr font;
|
||||
|
||||
/// <summary>
|
||||
/// Pushes the font, and return an instance of <see cref="SimplePushedFont"/>.
|
||||
/// </summary>
|
||||
/// <param name="stack">The <see cref="IFontHandle"/>-private stack.</param>
|
||||
/// <param name="fontPtr">The font pointer being pushed.</param>
|
||||
/// <returns>The rented instance of <see cref="SimplePushedFont"/>.</returns>
|
||||
public static SimplePushedFont Rent(List<IDisposable> stack, ImFontPtr fontPtr)
|
||||
{
|
||||
var rented = Pool.Get();
|
||||
Debug.Assert(rented.font.IsNull(), "Rented object must not have its font set");
|
||||
rented.stack = stack;
|
||||
|
||||
if (fontPtr.IsNotNullAndLoaded())
|
||||
{
|
||||
rented.font = fontPtr;
|
||||
ImGui.PushFont(fontPtr);
|
||||
}
|
||||
|
||||
return rented;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public unsafe void Dispose()
|
||||
{
|
||||
if (this.stack is null || !ReferenceEquals(this.stack[^1], this))
|
||||
{
|
||||
throw new InvalidOperationException("Tried to pop a non-pushed font.");
|
||||
}
|
||||
|
||||
this.stack.RemoveAt(this.stack.Count - 1);
|
||||
|
||||
if (!this.font.IsNull())
|
||||
{
|
||||
if (ImGui.GetFont().NativePtr == this.font.NativePtr)
|
||||
{
|
||||
ImGui.PopFont();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning(
|
||||
$"{nameof(IFontHandle.Pop)}: The font currently being popped does not match the pushed font. " +
|
||||
$"Doing nothing.");
|
||||
}
|
||||
}
|
||||
|
||||
this.font = default;
|
||||
this.stack = null;
|
||||
Pool.Return(this);
|
||||
}
|
||||
}
|
||||
203
Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs
Normal file
203
Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Deals with TrueType.
|
||||
/// </summary>
|
||||
internal static partial class TrueTypeUtils
|
||||
{
|
||||
private struct Fixed : IComparable<Fixed>
|
||||
{
|
||||
public ushort Major;
|
||||
public ushort Minor;
|
||||
|
||||
public Fixed(ushort major, ushort minor)
|
||||
{
|
||||
this.Major = major;
|
||||
this.Minor = minor;
|
||||
}
|
||||
|
||||
public Fixed(PointerSpan<byte> span)
|
||||
{
|
||||
var offset = 0;
|
||||
span.ReadBig(ref offset, out this.Major);
|
||||
span.ReadBig(ref offset, out this.Minor);
|
||||
}
|
||||
|
||||
public int CompareTo(Fixed other)
|
||||
{
|
||||
var majorComparison = this.Major.CompareTo(other.Major);
|
||||
return majorComparison != 0 ? majorComparison : this.Minor.CompareTo(other.Minor);
|
||||
}
|
||||
}
|
||||
|
||||
private struct KerningPair : IEquatable<KerningPair>
|
||||
{
|
||||
public ushort Left;
|
||||
public ushort Right;
|
||||
public short Value;
|
||||
|
||||
public KerningPair(PointerSpan<byte> span)
|
||||
{
|
||||
var offset = 0;
|
||||
span.ReadBig(ref offset, out this.Left);
|
||||
span.ReadBig(ref offset, out this.Right);
|
||||
span.ReadBig(ref offset, out this.Value);
|
||||
}
|
||||
|
||||
public KerningPair(ushort left, ushort right, short value)
|
||||
{
|
||||
this.Left = left;
|
||||
this.Right = right;
|
||||
this.Value = value;
|
||||
}
|
||||
|
||||
public static bool operator ==(KerningPair left, KerningPair right) => left.Equals(right);
|
||||
|
||||
public static bool operator !=(KerningPair left, KerningPair right) => !left.Equals(right);
|
||||
|
||||
public static KerningPair ReverseEndianness(KerningPair pair) => new()
|
||||
{
|
||||
Left = BinaryPrimitives.ReverseEndianness(pair.Left),
|
||||
Right = BinaryPrimitives.ReverseEndianness(pair.Right),
|
||||
Value = BinaryPrimitives.ReverseEndianness(pair.Value),
|
||||
};
|
||||
|
||||
public bool Equals(KerningPair other) =>
|
||||
this.Left == other.Left && this.Right == other.Right && this.Value == other.Value;
|
||||
|
||||
public override bool Equals(object? obj) => obj is KerningPair other && this.Equals(other);
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(this.Left, this.Right, this.Value);
|
||||
|
||||
public override string ToString() => $"KerningPair[{this.Left}, {this.Right}] = {this.Value}";
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit, Size = 4)]
|
||||
private struct PlatformAndEncoding
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
public PlatformId Platform;
|
||||
|
||||
[FieldOffset(2)]
|
||||
public UnicodeEncodingId UnicodeEncoding;
|
||||
|
||||
[FieldOffset(2)]
|
||||
public MacintoshEncodingId MacintoshEncoding;
|
||||
|
||||
[FieldOffset(2)]
|
||||
public IsoEncodingId IsoEncoding;
|
||||
|
||||
[FieldOffset(2)]
|
||||
public WindowsEncodingId WindowsEncoding;
|
||||
|
||||
public PlatformAndEncoding(PointerSpan<byte> source)
|
||||
{
|
||||
var offset = 0;
|
||||
source.ReadBig(ref offset, out this.Platform);
|
||||
source.ReadBig(ref offset, out this.UnicodeEncoding);
|
||||
}
|
||||
|
||||
public static PlatformAndEncoding ReverseEndianness(PlatformAndEncoding value) => new()
|
||||
{
|
||||
Platform = (PlatformId)BinaryPrimitives.ReverseEndianness((ushort)value.Platform),
|
||||
UnicodeEncoding = (UnicodeEncodingId)BinaryPrimitives.ReverseEndianness((ushort)value.UnicodeEncoding),
|
||||
};
|
||||
|
||||
public readonly string Decode(Span<byte> data)
|
||||
{
|
||||
switch (this.Platform)
|
||||
{
|
||||
case PlatformId.Unicode:
|
||||
switch (this.UnicodeEncoding)
|
||||
{
|
||||
case UnicodeEncodingId.Unicode_2_0_Bmp:
|
||||
case UnicodeEncodingId.Unicode_2_0_Full:
|
||||
return Encoding.BigEndianUnicode.GetString(data);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case PlatformId.Macintosh:
|
||||
switch (this.MacintoshEncoding)
|
||||
{
|
||||
case MacintoshEncodingId.Roman:
|
||||
return Encoding.ASCII.GetString(data);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case PlatformId.Windows:
|
||||
switch (this.WindowsEncoding)
|
||||
{
|
||||
case WindowsEncodingId.Symbol:
|
||||
case WindowsEncodingId.UnicodeBmp:
|
||||
case WindowsEncodingId.UnicodeFullRepertoire:
|
||||
return Encoding.BigEndianUnicode.GetString(data);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
private struct TagStruct : IEquatable<TagStruct>, IComparable<TagStruct>
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
public unsafe fixed byte Tag[4];
|
||||
|
||||
[FieldOffset(0)]
|
||||
public uint NativeValue;
|
||||
|
||||
public unsafe TagStruct(char c1, char c2, char c3, char c4)
|
||||
{
|
||||
this.Tag[0] = checked((byte)c1);
|
||||
this.Tag[1] = checked((byte)c2);
|
||||
this.Tag[2] = checked((byte)c3);
|
||||
this.Tag[3] = checked((byte)c4);
|
||||
}
|
||||
|
||||
public unsafe TagStruct(PointerSpan<byte> span)
|
||||
{
|
||||
this.Tag[0] = span[0];
|
||||
this.Tag[1] = span[1];
|
||||
this.Tag[2] = span[2];
|
||||
this.Tag[3] = span[3];
|
||||
}
|
||||
|
||||
public unsafe TagStruct(ReadOnlySpan<byte> span)
|
||||
{
|
||||
this.Tag[0] = span[0];
|
||||
this.Tag[1] = span[1];
|
||||
this.Tag[2] = span[2];
|
||||
this.Tag[3] = span[3];
|
||||
}
|
||||
|
||||
public unsafe byte this[int index]
|
||||
{
|
||||
get => this.Tag[index];
|
||||
set => this.Tag[index] = value;
|
||||
}
|
||||
|
||||
public static bool operator ==(TagStruct left, TagStruct right) => left.Equals(right);
|
||||
|
||||
public static bool operator !=(TagStruct left, TagStruct right) => !left.Equals(right);
|
||||
|
||||
public bool Equals(TagStruct other) => this.NativeValue == other.NativeValue;
|
||||
|
||||
public override bool Equals(object? obj) => obj is TagStruct other && this.Equals(other);
|
||||
|
||||
public override int GetHashCode() => (int)this.NativeValue;
|
||||
|
||||
public int CompareTo(TagStruct other) => this.NativeValue.CompareTo(other.NativeValue);
|
||||
|
||||
public override unsafe string ToString() =>
|
||||
$"0x{this.NativeValue:08X} \"{(char)this.Tag[0]}{(char)this.Tag[1]}{(char)this.Tag[2]}{(char)this.Tag[3]}\"";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Deals with TrueType.
|
||||
/// </summary>
|
||||
internal static partial class TrueTypeUtils
|
||||
{
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")]
|
||||
private enum IsoEncodingId : ushort
|
||||
{
|
||||
Ascii = 0,
|
||||
Iso_10646 = 1,
|
||||
Iso_8859_1 = 2,
|
||||
}
|
||||
|
||||
private enum MacintoshEncodingId : ushort
|
||||
{
|
||||
Roman = 0,
|
||||
}
|
||||
|
||||
private enum NameId : ushort
|
||||
{
|
||||
CopyrightNotice = 0,
|
||||
FamilyName = 1,
|
||||
SubfamilyName = 2,
|
||||
UniqueId = 3,
|
||||
FullFontName = 4,
|
||||
VersionString = 5,
|
||||
PostScriptName = 6,
|
||||
Trademark = 7,
|
||||
Manufacturer = 8,
|
||||
Designer = 9,
|
||||
Description = 10,
|
||||
UrlVendor = 11,
|
||||
UrlDesigner = 12,
|
||||
LicenseDescription = 13,
|
||||
LicenseInfoUrl = 14,
|
||||
TypographicFamilyName = 16,
|
||||
TypographicSubfamilyName = 17,
|
||||
CompatibleFullMac = 18,
|
||||
SampleText = 19,
|
||||
PoscSriptCidFindFontName = 20,
|
||||
WwsFamilyName = 21,
|
||||
WwsSubfamilyName = 22,
|
||||
LightBackgroundPalette = 23,
|
||||
DarkBackgroundPalette = 24,
|
||||
VariationPostScriptNamePrefix = 25,
|
||||
}
|
||||
|
||||
private enum PlatformId : ushort
|
||||
{
|
||||
Unicode = 0,
|
||||
Macintosh = 1, // discouraged
|
||||
Iso = 2, // deprecated
|
||||
Windows = 3,
|
||||
Custom = 4, // OTF Windows NT compatibility mapping
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")]
|
||||
private enum UnicodeEncodingId : ushort
|
||||
{
|
||||
Unicode_1_0 = 0, // deprecated
|
||||
Unicode_1_1 = 1, // deprecated
|
||||
IsoIec_10646 = 2, // deprecated
|
||||
Unicode_2_0_Bmp = 3,
|
||||
Unicode_2_0_Full = 4,
|
||||
UnicodeVariationSequences = 5,
|
||||
UnicodeFullRepertoire = 6,
|
||||
}
|
||||
|
||||
private enum WindowsEncodingId : ushort
|
||||
{
|
||||
Symbol = 0,
|
||||
UnicodeBmp = 1,
|
||||
ShiftJis = 2,
|
||||
Prc = 3,
|
||||
Big5 = 4,
|
||||
Wansung = 5,
|
||||
Johab = 6,
|
||||
UnicodeFullRepertoire = 10,
|
||||
}
|
||||
}
|
||||
148
Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs
Normal file
148
Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Deals with TrueType.
|
||||
/// </summary>
|
||||
[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")]
|
||||
[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")]
|
||||
[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")]
|
||||
[SuppressMessage(
|
||||
"StyleCop.CSharp.NamingRules",
|
||||
"SA1310:Field names should not contain underscore",
|
||||
Justification = "Version name")]
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name")]
|
||||
internal static partial class TrueTypeUtils
|
||||
{
|
||||
private readonly struct SfntFile : IReadOnlyDictionary<TagStruct, PointerSpan<byte>>
|
||||
{
|
||||
// http://formats.kaitai.io/ttf/ttf.svg
|
||||
|
||||
public static readonly TagStruct FileTagTrueType1 = new('1', '\0', '\0', '\0');
|
||||
public static readonly TagStruct FileTagType1 = new('t', 'y', 'p', '1');
|
||||
public static readonly TagStruct FileTagOpenTypeWithCff = new('O', 'T', 'T', 'O');
|
||||
public static readonly TagStruct FileTagOpenType1_0 = new('\0', '\x01', '\0', '\0');
|
||||
public static readonly TagStruct FileTagTrueTypeApple = new('t', 'r', 'u', 'e');
|
||||
|
||||
public readonly PointerSpan<byte> Memory;
|
||||
public readonly int OffsetInCollection;
|
||||
public readonly ushort TableCount;
|
||||
|
||||
public SfntFile(PointerSpan<byte> memory, int offsetInCollection = 0)
|
||||
{
|
||||
var span = memory.Span;
|
||||
this.Memory = memory;
|
||||
this.OffsetInCollection = offsetInCollection;
|
||||
this.TableCount = BinaryPrimitives.ReadUInt16BigEndian(span[4..]);
|
||||
}
|
||||
|
||||
public int Count => this.TableCount;
|
||||
|
||||
public IEnumerable<TagStruct> Keys => this.Select(x => x.Key);
|
||||
|
||||
public IEnumerable<PointerSpan<byte>> Values => this.Select(x => x.Value);
|
||||
|
||||
public PointerSpan<byte> this[TagStruct key] => this.First(x => x.Key == key).Value;
|
||||
|
||||
public IEnumerator<KeyValuePair<TagStruct, PointerSpan<byte>>> GetEnumerator()
|
||||
{
|
||||
var offset = 12;
|
||||
for (var i = 0; i < this.TableCount; i++)
|
||||
{
|
||||
var dte = new DirectoryTableEntry(this.Memory[offset..]);
|
||||
yield return new(dte.Tag, this.Memory.Slice(dte.Offset - this.OffsetInCollection, dte.Length));
|
||||
|
||||
offset += Unsafe.SizeOf<DirectoryTableEntry>();
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
public bool ContainsKey(TagStruct key) => this.Any(x => x.Key == key);
|
||||
|
||||
public bool TryGetValue(TagStruct key, out PointerSpan<byte> value)
|
||||
{
|
||||
foreach (var (k, v) in this)
|
||||
{
|
||||
if (k == key)
|
||||
{
|
||||
value = v;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public readonly struct DirectoryTableEntry
|
||||
{
|
||||
public readonly PointerSpan<byte> Memory;
|
||||
|
||||
public DirectoryTableEntry(PointerSpan<byte> span) => this.Memory = span;
|
||||
|
||||
public TagStruct Tag => new(this.Memory);
|
||||
|
||||
public uint Checksum => this.Memory.ReadU32Big(4);
|
||||
|
||||
public int Offset => this.Memory.ReadI32Big(8);
|
||||
|
||||
public int Length => this.Memory.ReadI32Big(12);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct TtcFile : IReadOnlyList<SfntFile>
|
||||
{
|
||||
public static readonly TagStruct FileTag = new('t', 't', 'c', 'f');
|
||||
|
||||
public readonly PointerSpan<byte> Memory;
|
||||
public readonly TagStruct Tag;
|
||||
public readonly ushort MajorVersion;
|
||||
public readonly ushort MinorVersion;
|
||||
public readonly int FontCount;
|
||||
|
||||
public TtcFile(PointerSpan<byte> memory)
|
||||
{
|
||||
var span = memory.Span;
|
||||
this.Memory = memory;
|
||||
this.Tag = new(span);
|
||||
if (this.Tag != FileTag)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
this.MajorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[4..]);
|
||||
this.MinorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[6..]);
|
||||
this.FontCount = BinaryPrimitives.ReadInt32BigEndian(span[8..]);
|
||||
}
|
||||
|
||||
public int Count => this.FontCount;
|
||||
|
||||
public SfntFile this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (index < 0 || index >= this.FontCount)
|
||||
{
|
||||
throw new IndexOutOfRangeException(
|
||||
$"The requested font #{index} does not exist in this .ttc file.");
|
||||
}
|
||||
|
||||
var offset = BinaryPrimitives.ReadInt32BigEndian(this.Memory.Span[(12 + 4 * index)..]);
|
||||
return new(this.Memory[offset..], offset);
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<SfntFile> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.FontCount; i++)
|
||||
yield return this[i];
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,259 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.Linq;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Deals with TrueType.
|
||||
/// </summary>
|
||||
internal static partial class TrueTypeUtils
|
||||
{
|
||||
[Flags]
|
||||
private enum LookupFlags : byte
|
||||
{
|
||||
RightToLeft = 1 << 0,
|
||||
IgnoreBaseGlyphs = 1 << 1,
|
||||
IgnoreLigatures = 1 << 2,
|
||||
IgnoreMarks = 1 << 3,
|
||||
UseMarkFilteringSet = 1 << 4,
|
||||
}
|
||||
|
||||
private enum LookupType : ushort
|
||||
{
|
||||
SingleAdjustment = 1,
|
||||
PairAdjustment = 2,
|
||||
CursiveAttachment = 3,
|
||||
MarkToBaseAttachment = 4,
|
||||
MarkToLigatureAttachment = 5,
|
||||
MarkToMarkAttachment = 6,
|
||||
ContextPositioning = 7,
|
||||
ChainedContextPositioning = 8,
|
||||
ExtensionPositioning = 9,
|
||||
}
|
||||
|
||||
private readonly struct ClassDefTable
|
||||
{
|
||||
public readonly PointerSpan<byte> Memory;
|
||||
|
||||
public ClassDefTable(PointerSpan<byte> memory) => this.Memory = memory;
|
||||
|
||||
public ushort Format => this.Memory.ReadU16Big(0);
|
||||
|
||||
public Format1ClassArray Format1 => new(this.Memory);
|
||||
|
||||
public Format2ClassRanges Format2 => new(this.Memory);
|
||||
|
||||
public IEnumerable<(ushort Class, ushort GlyphId)> Enumerate()
|
||||
{
|
||||
switch (this.Format)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
var format1 = this.Format1;
|
||||
var startId = format1.StartGlyphId;
|
||||
var count = format1.GlyphCount;
|
||||
var classes = format1.ClassValueArray;
|
||||
for (var i = 0; i < count; i++)
|
||||
yield return (classes[i], (ushort)(i + startId));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 2:
|
||||
{
|
||||
foreach (var range in this.Format2.ClassValueArray)
|
||||
{
|
||||
var @class = range.Class;
|
||||
var startId = range.StartGlyphId;
|
||||
var count = range.EndGlyphId - startId + 1;
|
||||
for (var i = 0; i < count; i++)
|
||||
yield return (@class, (ushort)(startId + i));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Pure]
|
||||
public ushort GetClass(ushort glyphId)
|
||||
{
|
||||
switch (this.Format)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
var format1 = this.Format1;
|
||||
var startId = format1.StartGlyphId;
|
||||
if (startId <= glyphId && glyphId < startId + format1.GlyphCount)
|
||||
return this.Format1.ClassValueArray[glyphId - startId];
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 2:
|
||||
{
|
||||
var rangeSpan = this.Format2.ClassValueArray;
|
||||
var i = rangeSpan.BinarySearch(new Format2ClassRanges.ClassRangeRecord { EndGlyphId = glyphId });
|
||||
if (i >= 0 && rangeSpan[i].ContainsGlyph(glyphId))
|
||||
return rangeSpan[i].Class;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public readonly struct Format1ClassArray
|
||||
{
|
||||
public readonly PointerSpan<byte> Memory;
|
||||
|
||||
public Format1ClassArray(PointerSpan<byte> memory) => this.Memory = memory;
|
||||
|
||||
public ushort Format => this.Memory.ReadU16Big(0);
|
||||
|
||||
public ushort StartGlyphId => this.Memory.ReadU16Big(2);
|
||||
|
||||
public ushort GlyphCount => this.Memory.ReadU16Big(4);
|
||||
|
||||
public BigEndianPointerSpan<ushort> ClassValueArray => new(
|
||||
this.Memory[6..].As<ushort>(this.GlyphCount),
|
||||
BinaryPrimitives.ReverseEndianness);
|
||||
}
|
||||
|
||||
public readonly struct Format2ClassRanges
|
||||
{
|
||||
public readonly PointerSpan<byte> Memory;
|
||||
|
||||
public Format2ClassRanges(PointerSpan<byte> memory) => this.Memory = memory;
|
||||
|
||||
public ushort ClassRangeCount => this.Memory.ReadU16Big(2);
|
||||
|
||||
public BigEndianPointerSpan<ClassRangeRecord> ClassValueArray => new(
|
||||
this.Memory[4..].As<ClassRangeRecord>(this.ClassRangeCount),
|
||||
ClassRangeRecord.ReverseEndianness);
|
||||
|
||||
public struct ClassRangeRecord : IComparable<ClassRangeRecord>
|
||||
{
|
||||
public ushort StartGlyphId;
|
||||
public ushort EndGlyphId;
|
||||
public ushort Class;
|
||||
|
||||
public static ClassRangeRecord ReverseEndianness(ClassRangeRecord value) => new()
|
||||
{
|
||||
StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId),
|
||||
EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId),
|
||||
Class = BinaryPrimitives.ReverseEndianness(value.Class),
|
||||
};
|
||||
|
||||
public int CompareTo(ClassRangeRecord other) => this.EndGlyphId.CompareTo(other.EndGlyphId);
|
||||
|
||||
public bool ContainsGlyph(ushort glyphId) =>
|
||||
this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct CoverageTable
|
||||
{
|
||||
public readonly PointerSpan<byte> Memory;
|
||||
|
||||
public CoverageTable(PointerSpan<byte> memory) => this.Memory = memory;
|
||||
|
||||
public enum CoverageFormat : ushort
|
||||
{
|
||||
Glyphs = 1,
|
||||
RangeRecords = 2,
|
||||
}
|
||||
|
||||
public CoverageFormat Format => this.Memory.ReadEnumBig<CoverageFormat>(0);
|
||||
|
||||
public ushort Count => this.Memory.ReadU16Big(2);
|
||||
|
||||
public BigEndianPointerSpan<ushort> Glyphs =>
|
||||
this.Format == CoverageFormat.Glyphs
|
||||
? new(this.Memory[4..].As<ushort>(this.Count), BinaryPrimitives.ReverseEndianness)
|
||||
: default(BigEndianPointerSpan<ushort>);
|
||||
|
||||
public BigEndianPointerSpan<RangeRecord> RangeRecords =>
|
||||
this.Format == CoverageFormat.RangeRecords
|
||||
? new(this.Memory[4..].As<RangeRecord>(this.Count), RangeRecord.ReverseEndianness)
|
||||
: default(BigEndianPointerSpan<RangeRecord>);
|
||||
|
||||
public int GetCoverageIndex(ushort glyphId)
|
||||
{
|
||||
switch (this.Format)
|
||||
{
|
||||
case CoverageFormat.Glyphs:
|
||||
return this.Glyphs.BinarySearch(glyphId);
|
||||
|
||||
case CoverageFormat.RangeRecords:
|
||||
{
|
||||
var index = this.RangeRecords.BinarySearch(
|
||||
(in RangeRecord record) => glyphId.CompareTo(record.EndGlyphId));
|
||||
|
||||
if (index >= 0 && this.RangeRecords[index].ContainsGlyph(glyphId))
|
||||
return index;
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public struct RangeRecord
|
||||
{
|
||||
public ushort StartGlyphId;
|
||||
public ushort EndGlyphId;
|
||||
public ushort StartCoverageIndex;
|
||||
|
||||
public static RangeRecord ReverseEndianness(RangeRecord value) => new()
|
||||
{
|
||||
StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId),
|
||||
EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId),
|
||||
StartCoverageIndex = BinaryPrimitives.ReverseEndianness(value.StartCoverageIndex),
|
||||
};
|
||||
|
||||
public bool ContainsGlyph(ushort glyphId) =>
|
||||
this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly struct LookupTable : IEnumerable<PointerSpan<byte>>
|
||||
{
|
||||
public readonly PointerSpan<byte> Memory;
|
||||
|
||||
public LookupTable(PointerSpan<byte> memory) => this.Memory = memory;
|
||||
|
||||
public LookupType Type => this.Memory.ReadEnumBig<LookupType>(0);
|
||||
|
||||
public byte MarkAttachmentType => this.Memory[2];
|
||||
|
||||
public LookupFlags Flags => (LookupFlags)this.Memory[3];
|
||||
|
||||
public ushort SubtableCount => this.Memory.ReadU16Big(4);
|
||||
|
||||
public BigEndianPointerSpan<ushort> SubtableOffsets => new(
|
||||
this.Memory[6..].As<ushort>(this.SubtableCount),
|
||||
BinaryPrimitives.ReverseEndianness);
|
||||
|
||||
public PointerSpan<byte> this[int index] => this.Memory[this.SubtableOffsets[this.EnsureIndex(index)] ..];
|
||||
|
||||
public IEnumerator<PointerSpan<byte>> GetEnumerator()
|
||||
{
|
||||
foreach (var i in Enumerable.Range(0, this.SubtableCount))
|
||||
yield return this.Memory[this.SubtableOffsets[i] ..];
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private int EnsureIndex(int index) => index >= 0 && index < this.SubtableCount
|
||||
? index
|
||||
: throw new IndexOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,443 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Deals with TrueType.
|
||||
/// </summary>
|
||||
internal static partial class TrueTypeUtils
|
||||
{
|
||||
private delegate int BinarySearchComparer<T>(in T value);
|
||||
|
||||
private static IDisposable CreatePointerSpan<T>(this T[] data, out PointerSpan<T> pointerSpan)
|
||||
where T : unmanaged
|
||||
{
|
||||
var gchandle = GCHandle.Alloc(data, GCHandleType.Pinned);
|
||||
pointerSpan = new(gchandle.AddrOfPinnedObject(), data.Length);
|
||||
return Disposable.Create(() => gchandle.Free());
|
||||
}
|
||||
|
||||
private static int BinarySearch<T>(this IReadOnlyList<T> span, in T value)
|
||||
where T : unmanaged, IComparable<T>
|
||||
{
|
||||
var l = 0;
|
||||
var r = span.Count - 1;
|
||||
while (l <= r)
|
||||
{
|
||||
var i = (int)(((uint)r + (uint)l) >> 1);
|
||||
var c = value.CompareTo(span[i]);
|
||||
switch (c)
|
||||
{
|
||||
case 0:
|
||||
return i;
|
||||
case > 0:
|
||||
l = i + 1;
|
||||
break;
|
||||
default:
|
||||
r = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ~l;
|
||||
}
|
||||
|
||||
private static int BinarySearch<T>(this IReadOnlyList<T> span, BinarySearchComparer<T> comparer)
|
||||
where T : unmanaged
|
||||
{
|
||||
var l = 0;
|
||||
var r = span.Count - 1;
|
||||
while (l <= r)
|
||||
{
|
||||
var i = (int)(((uint)r + (uint)l) >> 1);
|
||||
var c = comparer(span[i]);
|
||||
switch (c)
|
||||
{
|
||||
case 0:
|
||||
return i;
|
||||
case > 0:
|
||||
l = i + 1;
|
||||
break;
|
||||
default:
|
||||
r = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ~l;
|
||||
}
|
||||
|
||||
private static short ReadI16Big(this PointerSpan<byte> ps, int offset) =>
|
||||
BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]);
|
||||
|
||||
private static int ReadI32Big(this PointerSpan<byte> ps, int offset) =>
|
||||
BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]);
|
||||
|
||||
private static long ReadI64Big(this PointerSpan<byte> ps, int offset) =>
|
||||
BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]);
|
||||
|
||||
private static ushort ReadU16Big(this PointerSpan<byte> ps, int offset) =>
|
||||
BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]);
|
||||
|
||||
private static uint ReadU32Big(this PointerSpan<byte> ps, int offset) =>
|
||||
BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]);
|
||||
|
||||
private static ulong ReadU64Big(this PointerSpan<byte> ps, int offset) =>
|
||||
BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]);
|
||||
|
||||
private static Half ReadF16Big(this PointerSpan<byte> ps, int offset) =>
|
||||
BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]);
|
||||
|
||||
private static float ReadF32Big(this PointerSpan<byte> ps, int offset) =>
|
||||
BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]);
|
||||
|
||||
private static double ReadF64Big(this PointerSpan<byte> ps, int offset) =>
|
||||
BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]);
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, int offset, out short value) =>
|
||||
value = BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]);
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, int offset, out int value) =>
|
||||
value = BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]);
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, int offset, out long value) =>
|
||||
value = BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]);
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, int offset, out ushort value) =>
|
||||
value = BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]);
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, int offset, out uint value) =>
|
||||
value = BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]);
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, int offset, out ulong value) =>
|
||||
value = BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]);
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, int offset, out Half value) =>
|
||||
value = BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]);
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, int offset, out float value) =>
|
||||
value = BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]);
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, int offset, out double value) =>
|
||||
value = BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]);
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out short value)
|
||||
{
|
||||
ps.ReadBig(offset, out value);
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out int value)
|
||||
{
|
||||
ps.ReadBig(offset, out value);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out long value)
|
||||
{
|
||||
ps.ReadBig(offset, out value);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out ushort value)
|
||||
{
|
||||
ps.ReadBig(offset, out value);
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out uint value)
|
||||
{
|
||||
ps.ReadBig(offset, out value);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out ulong value)
|
||||
{
|
||||
ps.ReadBig(offset, out value);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out Half value)
|
||||
{
|
||||
ps.ReadBig(offset, out value);
|
||||
offset += 2;
|
||||
}
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out float value)
|
||||
{
|
||||
ps.ReadBig(offset, out value);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
private static void ReadBig(this PointerSpan<byte> ps, ref int offset, out double value)
|
||||
{
|
||||
ps.ReadBig(offset, out value);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
private static unsafe T ReadEnumBig<T>(this PointerSpan<byte> ps, int offset) where T : unmanaged, Enum
|
||||
{
|
||||
switch (Marshal.SizeOf(Enum.GetUnderlyingType(typeof(T))))
|
||||
{
|
||||
case 1:
|
||||
var b1 = ps.Span[offset];
|
||||
return *(T*)&b1;
|
||||
case 2:
|
||||
var b2 = ps.ReadU16Big(offset);
|
||||
return *(T*)&b2;
|
||||
case 4:
|
||||
var b4 = ps.ReadU32Big(offset);
|
||||
return *(T*)&b4;
|
||||
case 8:
|
||||
var b8 = ps.ReadU64Big(offset);
|
||||
return *(T*)&b8;
|
||||
default:
|
||||
throw new ArgumentException("Enum is not of size 1, 2, 4, or 8.", nameof(T), null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadBig<T>(this PointerSpan<byte> ps, int offset, out T value) where T : unmanaged, Enum =>
|
||||
value = ps.ReadEnumBig<T>(offset);
|
||||
|
||||
private static void ReadBig<T>(this PointerSpan<byte> ps, ref int offset, out T value) where T : unmanaged, Enum
|
||||
{
|
||||
value = ps.ReadEnumBig<T>(offset);
|
||||
offset += Unsafe.SizeOf<T>();
|
||||
}
|
||||
|
||||
private readonly unsafe struct PointerSpan<T> : IList<T>, IReadOnlyList<T>, ICollection
|
||||
where T : unmanaged
|
||||
{
|
||||
public readonly T* Pointer;
|
||||
|
||||
public PointerSpan(T* pointer, int count)
|
||||
{
|
||||
this.Pointer = pointer;
|
||||
this.Count = count;
|
||||
}
|
||||
|
||||
public PointerSpan(nint pointer, int count)
|
||||
: this((T*)pointer, count)
|
||||
{
|
||||
}
|
||||
|
||||
public Span<T> Span => new(this.Pointer, this.Count);
|
||||
|
||||
public bool IsEmpty => this.Count == 0;
|
||||
|
||||
public int Count { get; }
|
||||
|
||||
public int Length => this.Count;
|
||||
|
||||
public int ByteCount => sizeof(T) * this.Count;
|
||||
|
||||
bool ICollection.IsSynchronized => false;
|
||||
|
||||
object ICollection.SyncRoot => this;
|
||||
|
||||
bool ICollection<T>.IsReadOnly => false;
|
||||
|
||||
public ref T this[int index] => ref this.Pointer[this.EnsureIndex(index)];
|
||||
|
||||
public PointerSpan<T> this[Range range] => this.Slice(range.GetOffsetAndLength(this.Count));
|
||||
|
||||
T IList<T>.this[int index]
|
||||
{
|
||||
get => this.Pointer[this.EnsureIndex(index)];
|
||||
set => this.Pointer[this.EnsureIndex(index)] = value;
|
||||
}
|
||||
|
||||
T IReadOnlyList<T>.this[int index] => this.Pointer[this.EnsureIndex(index)];
|
||||
|
||||
public bool ContainsPointer<T2>(T2* obj) where T2 : unmanaged =>
|
||||
(T*)obj >= this.Pointer && (T*)(obj + 1) <= this.Pointer + this.Count;
|
||||
|
||||
public PointerSpan<T> Slice(int offset, int count) => new(this.Pointer + offset, count);
|
||||
|
||||
public PointerSpan<T> Slice((int Offset, int Count) offsetAndCount)
|
||||
=> this.Slice(offsetAndCount.Offset, offsetAndCount.Count);
|
||||
|
||||
public PointerSpan<T2> As<T2>(int count)
|
||||
where T2 : unmanaged =>
|
||||
count > this.Count / sizeof(T2)
|
||||
? throw new ArgumentOutOfRangeException(
|
||||
nameof(count),
|
||||
count,
|
||||
$"Wanted {count} items; had {this.Count / sizeof(T2)} items")
|
||||
: new((T2*)this.Pointer, count);
|
||||
|
||||
public PointerSpan<T2> As<T2>()
|
||||
where T2 : unmanaged =>
|
||||
new((T2*)this.Pointer, this.Count / sizeof(T2));
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Count; i++)
|
||||
yield return this[i];
|
||||
}
|
||||
|
||||
void ICollection<T>.Add(T item) => throw new NotSupportedException();
|
||||
|
||||
void ICollection<T>.Clear() => throw new NotSupportedException();
|
||||
|
||||
bool ICollection<T>.Contains(T item)
|
||||
{
|
||||
for (var i = 0; i < this.Count; i++)
|
||||
{
|
||||
if (Equals(this.Pointer[i], item))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
if (array.Length < this.Count)
|
||||
throw new ArgumentException(null, nameof(array));
|
||||
|
||||
if (array.Length < arrayIndex + this.Count)
|
||||
throw new ArgumentException(null, nameof(arrayIndex));
|
||||
|
||||
for (var i = 0; i < this.Count; i++)
|
||||
array[arrayIndex + i] = this.Pointer[i];
|
||||
}
|
||||
|
||||
bool ICollection<T>.Remove(T item) => throw new NotSupportedException();
|
||||
|
||||
int IList<T>.IndexOf(T item)
|
||||
{
|
||||
for (var i = 0; i < this.Count; i++)
|
||||
{
|
||||
if (Equals(this.Pointer[i], item))
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void IList<T>.Insert(int index, T item) => throw new NotSupportedException();
|
||||
|
||||
void IList<T>.RemoveAt(int index) => throw new NotSupportedException();
|
||||
|
||||
void ICollection.CopyTo(Array array, int arrayIndex)
|
||||
{
|
||||
if (array.Length < this.Count)
|
||||
throw new ArgumentException(null, nameof(array));
|
||||
|
||||
if (array.Length < arrayIndex + this.Count)
|
||||
throw new ArgumentException(null, nameof(arrayIndex));
|
||||
|
||||
for (var i = 0; i < this.Count; i++)
|
||||
array.SetValue(this.Pointer[i], arrayIndex + i);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private int EnsureIndex(int index) =>
|
||||
index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException();
|
||||
}
|
||||
|
||||
private readonly unsafe struct BigEndianPointerSpan<T>
|
||||
: IList<T>, IReadOnlyList<T>, ICollection
|
||||
where T : unmanaged
|
||||
{
|
||||
public readonly T* Pointer;
|
||||
|
||||
private readonly Func<T, T> reverseEndianness;
|
||||
|
||||
public BigEndianPointerSpan(PointerSpan<T> pointerSpan, Func<T, T> reverseEndianness)
|
||||
{
|
||||
this.reverseEndianness = reverseEndianness;
|
||||
this.Pointer = pointerSpan.Pointer;
|
||||
this.Count = pointerSpan.Count;
|
||||
}
|
||||
|
||||
public int Count { get; }
|
||||
|
||||
public int Length => this.Count;
|
||||
|
||||
public int ByteCount => sizeof(T) * this.Count;
|
||||
|
||||
public bool IsSynchronized => true;
|
||||
|
||||
public object SyncRoot => this;
|
||||
|
||||
public bool IsReadOnly => true;
|
||||
|
||||
public T this[int index]
|
||||
{
|
||||
get =>
|
||||
BitConverter.IsLittleEndian
|
||||
? this.reverseEndianness(this.Pointer[this.EnsureIndex(index)])
|
||||
: this.Pointer[this.EnsureIndex(index)];
|
||||
set => this.Pointer[this.EnsureIndex(index)] =
|
||||
BitConverter.IsLittleEndian
|
||||
? this.reverseEndianness(value)
|
||||
: value;
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < this.Count; i++)
|
||||
yield return this[i];
|
||||
}
|
||||
|
||||
void ICollection<T>.Add(T item) => throw new NotSupportedException();
|
||||
|
||||
void ICollection<T>.Clear() => throw new NotSupportedException();
|
||||
|
||||
bool ICollection<T>.Contains(T item) => throw new NotSupportedException();
|
||||
|
||||
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
|
||||
{
|
||||
if (array.Length < this.Count)
|
||||
throw new ArgumentException(null, nameof(array));
|
||||
|
||||
if (array.Length < arrayIndex + this.Count)
|
||||
throw new ArgumentException(null, nameof(arrayIndex));
|
||||
|
||||
for (var i = 0; i < this.Count; i++)
|
||||
array[arrayIndex + i] = this[i];
|
||||
}
|
||||
|
||||
bool ICollection<T>.Remove(T item) => throw new NotSupportedException();
|
||||
|
||||
int IList<T>.IndexOf(T item)
|
||||
{
|
||||
for (var i = 0; i < this.Count; i++)
|
||||
{
|
||||
if (Equals(this[i], item))
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void IList<T>.Insert(int index, T item) => throw new NotSupportedException();
|
||||
|
||||
void IList<T>.RemoveAt(int index) => throw new NotSupportedException();
|
||||
|
||||
void ICollection.CopyTo(Array array, int arrayIndex)
|
||||
{
|
||||
if (array.Length < this.Count)
|
||||
throw new ArgumentException(null, nameof(array));
|
||||
|
||||
if (array.Length < arrayIndex + this.Count)
|
||||
throw new ArgumentException(null, nameof(arrayIndex));
|
||||
|
||||
for (var i = 0; i < this.Count; i++)
|
||||
array.SetValue(this[i], arrayIndex + i);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
|
||||
|
||||
private int EnsureIndex(int index) =>
|
||||
index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException();
|
||||
}
|
||||
}
|
||||
1391
Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs
Normal file
1391
Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs
Normal file
File diff suppressed because it is too large
Load diff
135
Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs
Normal file
135
Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
using Dalamud.Interface.Utility;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// Deals with TrueType.
|
||||
/// </summary>
|
||||
internal static partial class TrueTypeUtils
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether the given <paramref name="fontConfig"/> will fail in <see cref="ImFontAtlasPtr.Build"/>,
|
||||
/// and throws an appropriate exception if it is the case.
|
||||
/// </summary>
|
||||
/// <param name="fontConfig">The font config.</param>
|
||||
public static unsafe void CheckImGuiCompatibleOrThrow(in ImFontConfig fontConfig)
|
||||
{
|
||||
var ranges = fontConfig.GlyphRanges;
|
||||
var sfnt = AsSfntFile(fontConfig);
|
||||
var cmap = new Cmap(sfnt);
|
||||
if (cmap.UnicodeTable is not { } unicodeTable)
|
||||
throw new NotSupportedException("The font does not have a compatible Unicode character mapping table.");
|
||||
if (unicodeTable.All(x => !ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(x.Key, ranges)))
|
||||
throw new NotSupportedException("The font does not have any glyph that falls under the requested range.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates through horizontal pair adjustments of a kern and gpos tables.
|
||||
/// </summary>
|
||||
/// <param name="fontConfig">The font config.</param>
|
||||
/// <returns>The enumerable of pair adjustments. Distance values need to be multiplied by font size in pixels.</returns>
|
||||
public static IEnumerable<(char Left, char Right, float Distance)> ExtractHorizontalPairAdjustments(
|
||||
ImFontConfig fontConfig)
|
||||
{
|
||||
float multiplier;
|
||||
Dictionary<ushort, char[]> glyphToCodepoints;
|
||||
Gpos gpos = default;
|
||||
Kern kern = default;
|
||||
|
||||
try
|
||||
{
|
||||
var sfnt = AsSfntFile(fontConfig);
|
||||
var head = new Head(sfnt);
|
||||
multiplier = 3f / 4 / head.UnitsPerEm;
|
||||
|
||||
if (new Cmap(sfnt).UnicodeTable is not { } table)
|
||||
yield break;
|
||||
|
||||
if (sfnt.ContainsKey(Kern.DirectoryTableTag))
|
||||
kern = new(sfnt);
|
||||
else if (sfnt.ContainsKey(Gpos.DirectoryTableTag))
|
||||
gpos = new(sfnt);
|
||||
else
|
||||
yield break;
|
||||
|
||||
glyphToCodepoints = table
|
||||
.GroupBy(x => x.Value, x => x.Key)
|
||||
.OrderBy(x => x.Key)
|
||||
.ToDictionary(
|
||||
x => x.Key,
|
||||
x => x.Where(y => y <= ushort.MaxValue)
|
||||
.Select(y => (char)y)
|
||||
.ToArray());
|
||||
}
|
||||
catch
|
||||
{
|
||||
// don't care; give up
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (kern.Memory.Count != 0)
|
||||
{
|
||||
foreach (var pair in kern.EnumerateHorizontalPairs())
|
||||
{
|
||||
if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars))
|
||||
continue;
|
||||
if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars))
|
||||
continue;
|
||||
|
||||
foreach (var l in leftChars)
|
||||
{
|
||||
foreach (var r in rightChars)
|
||||
yield return (l, r, pair.Value * multiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (gpos.Memory.Count != 0)
|
||||
{
|
||||
foreach (var pair in gpos.ExtractAdvanceX())
|
||||
{
|
||||
if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars))
|
||||
continue;
|
||||
if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars))
|
||||
continue;
|
||||
|
||||
foreach (var l in leftChars)
|
||||
{
|
||||
foreach (var r in rightChars)
|
||||
yield return (l, r, pair.Value * multiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe SfntFile AsSfntFile(in ImFontConfig fontConfig)
|
||||
{
|
||||
var memory = new PointerSpan<byte>((byte*)fontConfig.FontData, fontConfig.FontDataSize);
|
||||
if (memory.Length < 4)
|
||||
throw new NotSupportedException("File is too short to even have a magic.");
|
||||
|
||||
var magic = memory.ReadU32Big(0);
|
||||
if (BitConverter.IsLittleEndian)
|
||||
magic = BinaryPrimitives.ReverseEndianness(magic);
|
||||
|
||||
if (magic == SfntFile.FileTagTrueType1.NativeValue)
|
||||
return new(memory);
|
||||
if (magic == SfntFile.FileTagType1.NativeValue)
|
||||
return new(memory);
|
||||
if (magic == SfntFile.FileTagOpenTypeWithCff.NativeValue)
|
||||
return new(memory);
|
||||
if (magic == SfntFile.FileTagOpenType1_0.NativeValue)
|
||||
return new(memory);
|
||||
if (magic == SfntFile.FileTagTrueTypeApple.NativeValue)
|
||||
return new(memory);
|
||||
if (magic == TtcFile.FileTag.NativeValue)
|
||||
return new TtcFile(memory)[fontConfig.FontNo];
|
||||
|
||||
throw new NotSupportedException($"The given file with the magic 0x{magic:X08} is not supported.");
|
||||
}
|
||||
}
|
||||
306
Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs
Normal file
306
Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
using ImGuiNET;
|
||||
|
||||
namespace Dalamud.Interface.ManagedFontAtlas;
|
||||
|
||||
/// <summary>
|
||||
/// Managed version of <see cref="ImFontConfig"/>, to avoid unnecessary heap allocation and use of unsafe blocks.
|
||||
/// </summary>
|
||||
public struct SafeFontConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// The raw config.
|
||||
/// </summary>
|
||||
public ImFontConfig Raw;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SafeFontConfig"/> struct.
|
||||
/// </summary>
|
||||
public SafeFontConfig()
|
||||
{
|
||||
this.OversampleH = 1;
|
||||
this.OversampleV = 1;
|
||||
this.PixelSnapH = true;
|
||||
this.GlyphMaxAdvanceX = float.MaxValue;
|
||||
this.RasterizerMultiply = 1f;
|
||||
this.RasterizerGamma = 1.4f;
|
||||
this.EllipsisChar = unchecked((char)-1);
|
||||
this.Raw.FontDataOwnedByAtlas = 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SafeFontConfig"/> struct,
|
||||
/// copying applicable values from an existing instance of <see cref="ImFontConfigPtr"/>.
|
||||
/// </summary>
|
||||
/// <param name="config">Config to copy from.</param>
|
||||
public unsafe SafeFontConfig(ImFontConfigPtr config)
|
||||
: this()
|
||||
{
|
||||
if (config.NativePtr is not null)
|
||||
{
|
||||
this.Raw = *config.NativePtr;
|
||||
this.Raw.GlyphRanges = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of font within a TTF/OTF file.
|
||||
/// </summary>
|
||||
public int FontNo
|
||||
{
|
||||
get => this.Raw.FontNo;
|
||||
set => this.Raw.FontNo = EnsureRange(value, 0, int.MaxValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the desired size of the new font, in pixels.<br />
|
||||
/// Effectively, this is the line height.<br />
|
||||
/// Value is tied with <see cref="SizePt"/>.
|
||||
/// </summary>
|
||||
public float SizePx
|
||||
{
|
||||
get => this.Raw.SizePixels;
|
||||
set => this.Raw.SizePixels = EnsureRange(value, float.Epsilon, float.MaxValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the desired size of the new font, in points.<br />
|
||||
/// Effectively, this is the line height.<br />
|
||||
/// Value is tied with <see cref="SizePx"/>.
|
||||
/// </summary>
|
||||
public float SizePt
|
||||
{
|
||||
get => (this.Raw.SizePixels * 3) / 4;
|
||||
set => this.Raw.SizePixels = EnsureRange((value * 4) / 3, float.Epsilon, float.MaxValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the horizontal oversampling pixel count.<br />
|
||||
/// Rasterize at higher quality for sub-pixel positioning.<br />
|
||||
/// Note the difference between 2 and 3 is minimal so you can reduce this to 2 to save memory.<br />
|
||||
/// Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details.
|
||||
/// </summary>
|
||||
public int OversampleH
|
||||
{
|
||||
get => this.Raw.OversampleH;
|
||||
set => this.Raw.OversampleH = EnsureRange(value, 1, int.MaxValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vertical oversampling pixel count.<br />
|
||||
/// Rasterize at higher quality for sub-pixel positioning.<br />
|
||||
/// This is not really useful as we don't use sub-pixel positions on the Y axis.
|
||||
/// </summary>
|
||||
public int OversampleV
|
||||
{
|
||||
get => this.Raw.OversampleV;
|
||||
set => this.Raw.OversampleV = EnsureRange(value, 1, int.MaxValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to align every glyph to pixel boundary.<br />
|
||||
/// Useful e.g. if you are merging a non-pixel aligned font with the default font.<br />
|
||||
/// If enabled, you can set <see cref="OversampleH"/> and <see cref="OversampleV"/> to 1.
|
||||
/// </summary>
|
||||
public bool PixelSnapH
|
||||
{
|
||||
get => this.Raw.PixelSnapH != 0;
|
||||
set => this.Raw.PixelSnapH = value ? (byte)1 : (byte)0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the extra spacing (in pixels) between glyphs.<br />
|
||||
/// Only X axis is supported for now.<br />
|
||||
/// Effectively, it is the letter spacing.
|
||||
/// </summary>
|
||||
public Vector2 GlyphExtraSpacing
|
||||
{
|
||||
get => this.Raw.GlyphExtraSpacing;
|
||||
set => this.Raw.GlyphExtraSpacing = new(
|
||||
EnsureRange(value.X, float.MinValue, float.MaxValue),
|
||||
EnsureRange(value.Y, float.MinValue, float.MaxValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the offset all glyphs from this font input.<br />
|
||||
/// Use this to offset fonts vertically when merging multiple fonts.
|
||||
/// </summary>
|
||||
public Vector2 GlyphOffset
|
||||
{
|
||||
get => this.Raw.GlyphOffset;
|
||||
set => this.Raw.GlyphOffset = new(
|
||||
EnsureRange(value.X, float.MinValue, float.MaxValue),
|
||||
EnsureRange(value.Y, float.MinValue, float.MaxValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the glyph ranges, which is a user-provided list of Unicode range.
|
||||
/// Each range has 2 values, and values are inclusive.<br />
|
||||
/// The list must be zero-terminated.<br />
|
||||
/// If empty or null, then all the glyphs from the font that is in the range of UCS-2 will be added.
|
||||
/// </summary>
|
||||
public ushort[]? GlyphRanges { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum AdvanceX for glyphs.<br />
|
||||
/// Set only <see cref="GlyphMinAdvanceX"/> to align font icons.<br />
|
||||
/// Set both <see cref="GlyphMinAdvanceX"/>/<see cref="GlyphMaxAdvanceX"/> to enforce mono-space font.
|
||||
/// </summary>
|
||||
public float GlyphMinAdvanceX
|
||||
{
|
||||
get => this.Raw.GlyphMinAdvanceX;
|
||||
set => this.Raw.GlyphMinAdvanceX =
|
||||
float.IsFinite(value)
|
||||
? value
|
||||
: throw new ArgumentOutOfRangeException(
|
||||
nameof(value),
|
||||
value,
|
||||
$"{nameof(this.GlyphMinAdvanceX)} must be a finite number.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum AdvanceX for glyphs.
|
||||
/// </summary>
|
||||
public float GlyphMaxAdvanceX
|
||||
{
|
||||
get => this.Raw.GlyphMaxAdvanceX;
|
||||
set => this.Raw.GlyphMaxAdvanceX =
|
||||
float.IsFinite(value)
|
||||
? value
|
||||
: throw new ArgumentOutOfRangeException(
|
||||
nameof(value),
|
||||
value,
|
||||
$"{nameof(this.GlyphMaxAdvanceX)} must be a finite number.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that either brightens (>1.0f) or darkens (<1.0f) the font output.<br />
|
||||
/// Brightening small fonts may be a good workaround to make them more readable.
|
||||
/// </summary>
|
||||
public float RasterizerMultiply
|
||||
{
|
||||
get => this.Raw.RasterizerMultiply;
|
||||
set => this.Raw.RasterizerMultiply = EnsureRange(value, float.Epsilon, float.MaxValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the gamma value for fonts.
|
||||
/// </summary>
|
||||
public float RasterizerGamma
|
||||
{
|
||||
get => this.Raw.RasterizerGamma;
|
||||
set => this.Raw.RasterizerGamma = EnsureRange(value, float.Epsilon, float.MaxValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value explicitly specifying unicode codepoint of the ellipsis character.<br />
|
||||
/// When fonts are being merged first specified ellipsis will be used.
|
||||
/// </summary>
|
||||
public char EllipsisChar
|
||||
{
|
||||
get => (char)this.Raw.EllipsisChar;
|
||||
set => this.Raw.EllipsisChar = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the desired name of the new font. Names longer than 40 bytes will be partially lost.
|
||||
/// </summary>
|
||||
public unsafe string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (void* pName = this.Raw.Name)
|
||||
{
|
||||
var span = new ReadOnlySpan<byte>(pName, 40);
|
||||
var firstNull = span.IndexOf((byte)0);
|
||||
if (firstNull != -1)
|
||||
span = span[..firstNull];
|
||||
return Encoding.UTF8.GetString(span);
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
fixed (void* pName = this.Raw.Name)
|
||||
{
|
||||
var span = new Span<byte>(pName, 40);
|
||||
Encoding.UTF8.GetBytes(value, span);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the desired font to merge with, if set.
|
||||
/// </summary>
|
||||
public unsafe ImFontPtr MergeFont
|
||||
{
|
||||
get => this.Raw.DstFont is not null ? this.Raw.DstFont : default;
|
||||
set
|
||||
{
|
||||
this.Raw.MergeMode = value.NativePtr is null ? (byte)0 : (byte)1;
|
||||
this.Raw.DstFont = value.NativePtr is null ? default : value.NativePtr;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws <see cref="ArgumentException"/> with appropriate messages,
|
||||
/// if this <see cref="SafeFontConfig"/> has invalid values.
|
||||
/// </summary>
|
||||
public readonly void ThrowOnInvalidValues()
|
||||
{
|
||||
if (!(this.Raw.FontNo >= 0))
|
||||
throw new ArgumentException($"{nameof(this.FontNo)} must not be a negative number.");
|
||||
|
||||
if (!(this.Raw.SizePixels > 0))
|
||||
throw new ArgumentException($"{nameof(this.SizePx)} must be a positive number.");
|
||||
|
||||
if (!(this.Raw.OversampleH >= 1))
|
||||
throw new ArgumentException($"{nameof(this.OversampleH)} must be a negative number.");
|
||||
|
||||
if (!(this.Raw.OversampleV >= 1))
|
||||
throw new ArgumentException($"{nameof(this.OversampleV)} must be a negative number.");
|
||||
|
||||
if (!float.IsFinite(this.Raw.GlyphMinAdvanceX))
|
||||
throw new ArgumentException($"{nameof(this.GlyphMinAdvanceX)} must be a finite number.");
|
||||
|
||||
if (!float.IsFinite(this.Raw.GlyphMaxAdvanceX))
|
||||
throw new ArgumentException($"{nameof(this.GlyphMaxAdvanceX)} must be a finite number.");
|
||||
|
||||
if (!(this.Raw.RasterizerMultiply > 0))
|
||||
throw new ArgumentException($"{nameof(this.RasterizerMultiply)} must be a positive number.");
|
||||
|
||||
if (!(this.Raw.RasterizerGamma > 0))
|
||||
throw new ArgumentException($"{nameof(this.RasterizerGamma)} must be a positive number.");
|
||||
|
||||
if (this.GlyphRanges is { Length: > 0 } ranges)
|
||||
{
|
||||
if (ranges[0] == 0)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Font ranges cannot start with 0.",
|
||||
nameof(this.GlyphRanges));
|
||||
}
|
||||
|
||||
if (ranges[(ranges.Length - 1) & ~1] != 0)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Font ranges must terminate with a zero at even indices.",
|
||||
nameof(this.GlyphRanges));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static T EnsureRange<T>(T value, T min, T max, [CallerMemberName] string callerName = "")
|
||||
where T : INumber<T>
|
||||
{
|
||||
if (value < min)
|
||||
throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be less than {min}.");
|
||||
if (value > max)
|
||||
throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be more than {max}.");
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -12,6 +11,9 @@ using Dalamud.Interface.GameFonts;
|
|||
using Dalamud.Interface.Internal;
|
||||
using Dalamud.Interface.Internal.ManagedAsserts;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Plugin.Internal.Types;
|
||||
using Dalamud.Utility;
|
||||
using ImGuiNET;
|
||||
using ImGuiScene;
|
||||
|
|
@ -30,14 +32,20 @@ public sealed class UiBuilder : IDisposable
|
|||
private readonly HitchDetector hitchDetector;
|
||||
private readonly string namespaceName;
|
||||
private readonly InterfaceManager interfaceManager = Service<InterfaceManager>.Get();
|
||||
private readonly GameFontManager gameFontManager = Service<GameFontManager>.Get();
|
||||
private readonly Framework framework = Service<Framework>.Get();
|
||||
|
||||
[ServiceManager.ServiceDependency]
|
||||
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
|
||||
|
||||
private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new();
|
||||
|
||||
private bool hasErrorWindow = false;
|
||||
private bool lastFrameUiHideState = false;
|
||||
|
||||
private IFontHandle? defaultFontHandle;
|
||||
private IFontHandle? iconFontHandle;
|
||||
private IFontHandle? monoFontHandle;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UiBuilder"/> class and registers it.
|
||||
/// You do not have to call this manually.
|
||||
|
|
@ -45,14 +53,32 @@ public sealed class UiBuilder : IDisposable
|
|||
/// <param name="namespaceName">The plugin namespace.</param>
|
||||
internal UiBuilder(string namespaceName)
|
||||
{
|
||||
this.stopwatch = new Stopwatch();
|
||||
this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch);
|
||||
this.namespaceName = namespaceName;
|
||||
try
|
||||
{
|
||||
this.stopwatch = new Stopwatch();
|
||||
this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch);
|
||||
this.namespaceName = namespaceName;
|
||||
|
||||
this.interfaceManager.Draw += this.OnDraw;
|
||||
this.interfaceManager.BuildFonts += this.OnBuildFonts;
|
||||
this.interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts;
|
||||
this.interfaceManager.ResizeBuffers += this.OnResizeBuffers;
|
||||
this.interfaceManager.Draw += this.OnDraw;
|
||||
this.scopedFinalizer.Add(() => this.interfaceManager.Draw -= this.OnDraw);
|
||||
|
||||
this.interfaceManager.ResizeBuffers += this.OnResizeBuffers;
|
||||
this.scopedFinalizer.Add(() => this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers);
|
||||
|
||||
this.FontAtlas =
|
||||
this.scopedFinalizer
|
||||
.Add(
|
||||
Service<FontAtlasFactory>
|
||||
.Get()
|
||||
.CreateFontAtlas(namespaceName, FontAtlasAutoRebuildMode.Disable));
|
||||
this.FontAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange;
|
||||
this.FontAtlas.RebuildRecommend += this.RebuildFonts;
|
||||
}
|
||||
catch
|
||||
{
|
||||
this.scopedFinalizer.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -78,21 +104,59 @@ public sealed class UiBuilder : IDisposable
|
|||
|
||||
/// <summary>
|
||||
/// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.<br/>
|
||||
/// Any ImFontPtr objects that you store <strong>can be invalidated</strong> when fonts are rebuilt
|
||||
/// Any ImFontPtr objects that you store <b>can be invalidated</b> 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>
|
||||
/// pointers inside this handler.
|
||||
/// </summary>
|
||||
public event Action BuildFonts;
|
||||
/// <remarks>
|
||||
/// To add your custom font, use <see cref="FontAtlas"/>.<see cref="IFontAtlas.NewDelegateFontHandle"/> or
|
||||
/// <see cref="IFontAtlas.NewGameFontHandle"/>.<br />
|
||||
/// To be notified on font changes after fonts are built, use
|
||||
/// <see cref="DefaultFontHandle"/>.<see cref="IFontHandle.ImFontChanged"/>.<br />
|
||||
/// For all other purposes, use <see cref="FontAtlas"/>.<see cref="IFontAtlas.BuildStepChange"/>.<br />
|
||||
/// <br />
|
||||
/// Note that you will be calling above functions once, instead of every time inside a build step change callback.
|
||||
/// For example, you can make all font handles from your plugin constructor, and then use the created handles during
|
||||
/// <see cref="Draw"/> event, by using <see cref="IFontHandle.Push"/> in a scope.<br />
|
||||
/// You may dispose your font handle anytime, as long as it's not in use in <see cref="Draw"/>.
|
||||
/// Font handles may be constructed anytime, as long as the owner <see cref="IFontAtlas"/> or
|
||||
/// <see cref="UiBuilder"/> is not disposed.<br />
|
||||
/// <br />
|
||||
/// If you were storing <see cref="ImFontPtr"/>, consider if the job can be achieved solely by using
|
||||
/// <see cref="IFontHandle"/> without directly using an instance of <see cref="ImFontPtr"/>.<br />
|
||||
/// If you do need it, evaluate if you need to access fonts outside the main thread.<br />
|
||||
/// If it is the case, use <see cref="IFontHandle.Lock"/> to obtain a safe-to-access instance of
|
||||
/// <see cref="ImFontPtr"/>, once <see cref="IFontHandle.WaitAsync"/> resolves.<br />
|
||||
/// Otherwise, use <see cref="IFontHandle.Push"/>, and obtain the instance of <see cref="ImFontPtr"/> via
|
||||
/// <see cref="ImGui.GetFont"/>. Do not let the <see cref="ImFontPtr"/> escape the <c>using</c> scope.<br />
|
||||
/// <br />
|
||||
/// If your plugin sets <see cref="PluginManifest.LoadRequiredState"/> to a non-default value, then
|
||||
/// <see cref="DefaultFontHandle"/> should be accessed using
|
||||
/// <see cref="RunWhenUiPrepared{T}(System.Func{T},bool)"/>, as the font handle member variables are only available
|
||||
/// once drawing facilities are available.<br />
|
||||
/// <br />
|
||||
/// <b>Examples:</b><br />
|
||||
/// * <see cref="InterfaceManager.ContinueConstruction"/>.<br />
|
||||
/// * <see cref="Interface.Internal.Windows.Data.Widgets.GamePrebakedFontsTestWidget"/>.<br />
|
||||
/// * <see cref="Interface.Internal.Windows.TitleScreenMenuWindow"/> ctor.<br />
|
||||
/// * <see cref="Interface.Internal.Windows.Settings.Tabs.SettingsTabAbout"/>:
|
||||
/// note how the construction of a new instance of <see cref="IFontAtlas"/> and
|
||||
/// call of <see cref="IFontAtlas.NewGameFontHandle"/> are done in different functions,
|
||||
/// without having to manually initiate font rebuild process.
|
||||
/// </remarks>
|
||||
[Obsolete("See remarks.", false)]
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
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
|
||||
/// Any ImFontPtr objects that you store <b>can be invalidated</b> 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>
|
||||
/// pointers inside this handler.
|
||||
/// </summary>
|
||||
public event Action AfterBuildFonts;
|
||||
[Obsolete($"See remarks for {nameof(BuildFonts)}.", false)]
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public event Action? AfterBuildFonts;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an action that is called when plugin UI or interface modifications are supposed to be shown.
|
||||
|
|
@ -107,20 +171,89 @@ public sealed class UiBuilder : IDisposable
|
|||
public event Action HideUi;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons.
|
||||
/// Gets the default Dalamud font size in points.
|
||||
/// </summary>
|
||||
public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default Dalamud font size in pixels.
|
||||
/// </summary>
|
||||
public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default Dalamud font - supporting all game languages and icons.<br />
|
||||
/// <strong>Accessing this static property outside of <see cref="Draw"/> is dangerous and not supported.</strong>
|
||||
/// </summary>
|
||||
public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default Dalamud icon font based on FontAwesome 5 Free solid in 17pt.
|
||||
/// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.<br />
|
||||
/// <strong>Accessing this static property outside of <see cref="Draw"/> is dangerous and not supported.</strong>
|
||||
/// </summary>
|
||||
public static ImFontPtr IconFont => InterfaceManager.IconFont;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default Dalamud monospaced font based on Inconsolata Regular in 16pt.
|
||||
/// Gets the default Dalamud monospaced font based on Inconsolata Regular.<br />
|
||||
/// <strong>Accessing this static property outside of <see cref="Draw"/> is dangerous and not supported.</strong>
|
||||
/// </summary>
|
||||
public static ImFontPtr MonoFont => InterfaceManager.MonoFont;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the handle to the default Dalamud font - supporting all game languages and icons.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A font handle corresponding to this font can be obtained with:
|
||||
/// <code>
|
||||
/// fontAtlas.NewDelegateFontHandle(
|
||||
/// e => e.OnPreBuild(
|
||||
/// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt)));
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public IFontHandle DefaultFontHandle =>
|
||||
this.defaultFontHandle ??=
|
||||
this.scopedFinalizer.Add(
|
||||
new FontHandleWrapper(
|
||||
this.InterfaceManagerWithScene?.DefaultFontHandle
|
||||
?? throw new InvalidOperationException("Scene is not yet ready.")));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A font handle corresponding to this font can be obtained with:
|
||||
/// <code>
|
||||
/// fontAtlas.NewDelegateFontHandle(
|
||||
/// e => e.OnPreBuild(
|
||||
/// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt })));
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public IFontHandle IconFontHandle =>
|
||||
this.iconFontHandle ??=
|
||||
this.scopedFinalizer.Add(
|
||||
new FontHandleWrapper(
|
||||
this.InterfaceManagerWithScene?.IconFontHandle
|
||||
?? throw new InvalidOperationException("Scene is not yet ready.")));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default Dalamud monospaced font based on Inconsolata Regular.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A font handle corresponding to this font can be obtained with:
|
||||
/// <code>
|
||||
/// fontAtlas.NewDelegateFontHandle(
|
||||
/// e => e.OnPreBuild(
|
||||
/// tk => tk.AddDalamudAssetFont(
|
||||
/// DalamudAsset.InconsolataRegular,
|
||||
/// new() { SizePt = UiBuilder.DefaultFontSizePt })));
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public IFontHandle MonoFontHandle =>
|
||||
this.monoFontHandle ??=
|
||||
this.scopedFinalizer.Add(
|
||||
new FontHandleWrapper(
|
||||
this.InterfaceManagerWithScene?.MonoFontHandle
|
||||
?? throw new InvalidOperationException("Scene is not yet ready.")));
|
||||
|
||||
/// <summary>
|
||||
/// Gets the game's active Direct3D device.
|
||||
/// </summary>
|
||||
|
|
@ -190,6 +323,11 @@ public sealed class UiBuilder : IDisposable
|
|||
/// </summary>
|
||||
public bool UiPrepared => Service<InterfaceManager.InterfaceManagerWithScene>.GetNullable() != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin-private font atlas.
|
||||
/// </summary>
|
||||
public IFontAtlas FontAtlas { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether statistics about UI draw time should be collected.
|
||||
/// </summary>
|
||||
|
|
@ -319,7 +457,7 @@ public sealed class UiBuilder : IDisposable
|
|||
if (runInFrameworkThread)
|
||||
{
|
||||
return this.InterfaceManagerWithSceneAsync
|
||||
.ContinueWith(_ => Service<Framework>.Get().RunOnFrameworkThread(func))
|
||||
.ContinueWith(_ => this.framework.RunOnFrameworkThread(func))
|
||||
.Unwrap();
|
||||
}
|
||||
else
|
||||
|
|
@ -341,7 +479,7 @@ public sealed class UiBuilder : IDisposable
|
|||
if (runInFrameworkThread)
|
||||
{
|
||||
return this.InterfaceManagerWithSceneAsync
|
||||
.ContinueWith(_ => Service<Framework>.Get().RunOnFrameworkThread(func))
|
||||
.ContinueWith(_ => this.framework.RunOnFrameworkThread(func))
|
||||
.Unwrap();
|
||||
}
|
||||
else
|
||||
|
|
@ -357,19 +495,50 @@ public sealed class UiBuilder : IDisposable
|
|||
/// </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) => this.gameFontManager.NewFontRef(style);
|
||||
[Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)]
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public GameFontHandle GetGameFontHandle(GameFontStyle style) => new(
|
||||
(GamePrebakedFontHandle)this.FontAtlas.NewGameFontHandle(style),
|
||||
Service<FontAtlasFactory>.Get());
|
||||
|
||||
/// <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
|
||||
/// ready to be used on the next UI frame.
|
||||
/// This will invoke any <see cref="BuildFonts"/> and <see cref="AfterBuildFonts"/> handlers and ensure that any
|
||||
/// loaded fonts are ready to be used on the next UI frame.
|
||||
/// </summary>
|
||||
public void RebuildFonts()
|
||||
{
|
||||
Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName);
|
||||
this.interfaceManager.RebuildFonts();
|
||||
if (this.AfterBuildFonts is null && this.BuildFonts is null)
|
||||
this.FontAtlas.BuildFontsAsync();
|
||||
else
|
||||
this.FontAtlas.BuildFontsOnNextFrame();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an isolated <see cref="IFontAtlas"/>.
|
||||
/// </summary>
|
||||
/// <param name="autoRebuildMode">Specify when and how to rebuild this atlas.</param>
|
||||
/// <param name="isGlobalScaled">Whether the fonts in the atlas is global scaled.</param>
|
||||
/// <param name="debugName">Name for debugging purposes.</param>
|
||||
/// <returns>A new instance of <see cref="IFontAtlas"/>.</returns>
|
||||
/// <remarks>
|
||||
/// Use this to create extra font atlases, if you want to create and dispose fonts without having to rebuild all
|
||||
/// other fonts together.<br />
|
||||
/// If <paramref name="autoRebuildMode"/> is not <see cref="FontAtlasAutoRebuildMode.OnNewFrame"/>,
|
||||
/// the font rebuilding functions must be called manually.
|
||||
/// </remarks>
|
||||
public IFontAtlas CreateFontAtlas(
|
||||
FontAtlasAutoRebuildMode autoRebuildMode,
|
||||
bool isGlobalScaled = true,
|
||||
string? debugName = null) =>
|
||||
this.scopedFinalizer.Add(Service<FontAtlasFactory>
|
||||
.Get()
|
||||
.CreateFontAtlas(
|
||||
this.namespaceName + ":" + (debugName ?? "custom"),
|
||||
autoRebuildMode,
|
||||
isGlobalScaled));
|
||||
|
||||
/// <summary>
|
||||
/// Add a notification to the notification queue.
|
||||
/// </summary>
|
||||
|
|
@ -392,12 +561,7 @@ public sealed class UiBuilder : IDisposable
|
|||
/// <summary>
|
||||
/// Unregister the UiBuilder. Do not call this in plugin code.
|
||||
/// </summary>
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
this.interfaceManager.Draw -= this.OnDraw;
|
||||
this.interfaceManager.BuildFonts -= this.OnBuildFonts;
|
||||
this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers;
|
||||
}
|
||||
void IDisposable.Dispose() => this.scopedFinalizer.Dispose();
|
||||
|
||||
/// <summary>
|
||||
/// Open the registered configuration UI, if it exists.
|
||||
|
|
@ -463,8 +627,12 @@ public sealed class UiBuilder : IDisposable
|
|||
this.ShowUi?.InvokeSafely();
|
||||
}
|
||||
|
||||
if (!this.interfaceManager.FontsReady)
|
||||
// just in case, if something goes wrong, prevent drawing; otherwise it probably will crash.
|
||||
if (!this.FontAtlas.BuildTask.IsCompletedSuccessfully
|
||||
&& (this.BuildFonts is not null || this.AfterBuildFonts is not null))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.PushID(this.namespaceName);
|
||||
if (DoStats)
|
||||
|
|
@ -526,18 +694,89 @@ public sealed class UiBuilder : IDisposable
|
|||
this.hitchDetector.Stop();
|
||||
}
|
||||
|
||||
private void OnBuildFonts()
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
private unsafe void PrivateAtlasOnBuildStepChange(IFontAtlasBuildToolkit e)
|
||||
{
|
||||
this.BuildFonts?.InvokeSafely();
|
||||
}
|
||||
if (e.IsAsyncBuildOperation)
|
||||
return;
|
||||
|
||||
private void OnAfterBuildFonts()
|
||||
{
|
||||
this.AfterBuildFonts?.InvokeSafely();
|
||||
ThreadSafety.AssertMainThread();
|
||||
|
||||
if (this.BuildFonts is not null)
|
||||
{
|
||||
e.OnPreBuild(
|
||||
_ =>
|
||||
{
|
||||
var prev = ImGui.GetIO().NativePtr->Fonts;
|
||||
ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr;
|
||||
((IFontAtlasBuildToolkit.IApi9Compat)e)
|
||||
.FromUiBuilderObsoleteEventHandlers(() => this.BuildFonts?.InvokeSafely());
|
||||
ImGui.GetIO().NativePtr->Fonts = prev;
|
||||
});
|
||||
}
|
||||
|
||||
if (this.AfterBuildFonts is not null)
|
||||
{
|
||||
e.OnPostBuild(
|
||||
_ =>
|
||||
{
|
||||
var prev = ImGui.GetIO().NativePtr->Fonts;
|
||||
ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr;
|
||||
((IFontAtlasBuildToolkit.IApi9Compat)e)
|
||||
.FromUiBuilderObsoleteEventHandlers(() => this.AfterBuildFonts?.InvokeSafely());
|
||||
ImGui.GetIO().NativePtr->Fonts = prev;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResizeBuffers()
|
||||
{
|
||||
this.ResizeBuffers?.InvokeSafely();
|
||||
}
|
||||
|
||||
private class FontHandleWrapper : IFontHandle
|
||||
{
|
||||
private IFontHandle? wrapped;
|
||||
|
||||
public FontHandleWrapper(IFontHandle wrapped)
|
||||
{
|
||||
this.wrapped = wrapped;
|
||||
this.wrapped.ImFontChanged += this.WrappedOnImFontChanged;
|
||||
}
|
||||
|
||||
public event IFontHandle.ImFontChangedDelegate? ImFontChanged;
|
||||
|
||||
public Exception? LoadException => this.WrappedNotDisposed.LoadException;
|
||||
|
||||
public bool Available => this.WrappedNotDisposed.Available;
|
||||
|
||||
private IFontHandle WrappedNotDisposed =>
|
||||
this.wrapped ?? throw new ObjectDisposedException(nameof(FontHandleWrapper));
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.wrapped is not { } w)
|
||||
return;
|
||||
|
||||
this.wrapped = null;
|
||||
w.ImFontChanged -= this.WrappedOnImFontChanged;
|
||||
// Note: do not dispose w; we do not own it
|
||||
}
|
||||
|
||||
public ILockedImFont Lock() =>
|
||||
this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper));
|
||||
|
||||
public IDisposable Push() => this.WrappedNotDisposed.Push();
|
||||
|
||||
public void Pop() => this.WrappedNotDisposed.Pop();
|
||||
|
||||
public Task<IFontHandle> WaitAsync() =>
|
||||
this.WrappedNotDisposed.WaitAsync().ContinueWith(_ => (IFontHandle)this);
|
||||
|
||||
public override string ToString() =>
|
||||
$"{nameof(FontHandleWrapper)}({this.wrapped?.ToString() ?? "disposed"})";
|
||||
|
||||
private void WrappedOnImFontChanged(IFontHandle obj, ILockedImFont lockedFont) =>
|
||||
this.ImFontChanged?.Invoke(obj, lockedFont);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ public class UldWrapper : IDisposable
|
|||
|
||||
private IDalamudTextureWrap? CopyRect(int width, int height, byte[] rgbaData, UldRoot.PartData part)
|
||||
{
|
||||
if (part.V + part.W > width || part.U + part.H > height)
|
||||
if (part.U + part.W > width || part.V + part.H > height)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Unicode;
|
||||
|
||||
using Dalamud.Configuration.Internal;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using Dalamud.Interface.ManagedFontAtlas;
|
||||
using Dalamud.Interface.ManagedFontAtlas.Internals;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using ImGuiNET;
|
||||
using ImGuiScene;
|
||||
|
|
@ -31,8 +36,7 @@ public static class ImGuiHelpers
|
|||
/// This does not necessarily mean you can call drawing functions.
|
||||
/// </summary>
|
||||
public static unsafe bool IsImGuiInitialized =>
|
||||
ImGui.GetCurrentContext() is not (nint)0 // KW: IDEs get mad without the cast, despite being unnecessary
|
||||
&& ImGui.GetIO().NativePtr is not null;
|
||||
ImGui.GetCurrentContext() != nint.Zero && ImGui.GetIO().NativePtr is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the global Dalamud scale; even available before drawing is ready.<br />
|
||||
|
|
@ -198,7 +202,7 @@ public static class ImGuiHelpers
|
|||
/// <param name="round">If a positive number is given, numbers will be rounded to this.</param>
|
||||
public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f)
|
||||
{
|
||||
Func<float, float> rounder = round > 0 ? x => MathF.Round(x * round) / round : x => x;
|
||||
Func<float, float> rounder = round > 0 ? x => MathF.Round(x / round) * round : x => x;
|
||||
|
||||
var font = fontPtr.NativePtr;
|
||||
font->FontSize = rounder(font->FontSize * scale);
|
||||
|
|
@ -310,6 +314,7 @@ public static class ImGuiHelpers
|
|||
glyph->U1,
|
||||
glyph->V1,
|
||||
glyph->AdvanceX * scale);
|
||||
target.Mark4KPageUsedAfterGlyphAdd((ushort)glyph->Codepoint);
|
||||
changed = true;
|
||||
}
|
||||
else if (!missingOnly)
|
||||
|
|
@ -343,25 +348,18 @@ public static class ImGuiHelpers
|
|||
}
|
||||
|
||||
if (changed && rebuildLookupTable)
|
||||
target.BuildLookupTableNonstandard();
|
||||
}
|
||||
{
|
||||
// ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph.
|
||||
// FallbackGlyph is resolved after resolving ' '.
|
||||
// On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null,
|
||||
// making FindGlyph return nullptr.
|
||||
// On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null,
|
||||
// making ImGui attempt to treat whatever was there as a ' '.
|
||||
// This may cause random glyphs to be sized randomly, if not an access violation exception.
|
||||
target.NativePtr->FallbackGlyph = null;
|
||||
|
||||
/// <summary>
|
||||
/// Call ImFont::BuildLookupTable, after attempting to fulfill some preconditions.
|
||||
/// </summary>
|
||||
/// <param name="font">The font.</param>
|
||||
public static unsafe void BuildLookupTableNonstandard(this ImFontPtr font)
|
||||
{
|
||||
// ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph.
|
||||
// FallbackGlyph is resolved after resolving ' '.
|
||||
// On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null,
|
||||
// making FindGlyph return nullptr.
|
||||
// On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null,
|
||||
// making ImGui attempt to treat whatever was there as a ' '.
|
||||
// This may cause random glyphs to be sized randomly, if not an access violation exception.
|
||||
font.NativePtr->FallbackGlyph = null;
|
||||
|
||||
font.BuildLookupTable();
|
||||
target.BuildLookupTable();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -407,6 +405,103 @@ public static class ImGuiHelpers
|
|||
public static void CenterCursorFor(float itemWidth) =>
|
||||
ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2));
|
||||
|
||||
/// <summary>
|
||||
/// Allocates memory on the heap using <see cref="ImGuiNative.igMemAlloc"/><br />
|
||||
/// Memory must be freed using <see cref="ImGuiNative.igMemFree"/>.
|
||||
/// <br />
|
||||
/// Note that null is a valid return value when <paramref name="length"/> is 0.
|
||||
/// </summary>
|
||||
/// <param name="length">The length of allocated memory.</param>
|
||||
/// <returns>The allocated memory.</returns>
|
||||
/// <exception cref="OutOfMemoryException">If <see cref="ImGuiNative.igMemAlloc"/> returns null.</exception>
|
||||
public static unsafe void* AllocateMemory(int length)
|
||||
{
|
||||
// TODO: igMemAlloc takes size_t, which is nint; ImGui.NET apparently interpreted that as uint.
|
||||
// fix that in ImGui.NET.
|
||||
switch (length)
|
||||
{
|
||||
case 0:
|
||||
return null;
|
||||
case < 0:
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(length),
|
||||
length,
|
||||
$"{nameof(length)} cannot be a negative number.");
|
||||
default:
|
||||
var memory = ImGuiNative.igMemAlloc((uint)length);
|
||||
if (memory is null)
|
||||
{
|
||||
throw new OutOfMemoryException(
|
||||
$"Failed to allocate {length} bytes using {nameof(ImGuiNative.igMemAlloc)}");
|
||||
}
|
||||
|
||||
return memory;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ImFontGlyphRangesBuilderPtr"/> with a natively backed memory.
|
||||
/// </summary>
|
||||
/// <param name="builder">The created instance.</param>
|
||||
/// <returns>Disposable you can call.</returns>
|
||||
public static unsafe IDisposable NewFontGlyphRangeBuilderPtrScoped(out ImFontGlyphRangesBuilderPtr builder)
|
||||
{
|
||||
builder = new(ImGuiNative.ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder());
|
||||
var ptr = builder.NativePtr;
|
||||
return Disposable.Create(() =>
|
||||
{
|
||||
if (ptr != null)
|
||||
ImGuiNative.ImFontGlyphRangesBuilder_destroy(ptr);
|
||||
ptr = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds ImGui Glyph Ranges for use with <see cref="SafeFontConfig.GlyphRanges"/>.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder.</param>
|
||||
/// <param name="addFallbackCodepoints">Add fallback codepoints to the range.</param>
|
||||
/// <param name="addEllipsisCodepoints">Add ellipsis codepoints to the range.</param>
|
||||
/// <returns>When disposed, the resource allocated for the range will be freed.</returns>
|
||||
public static unsafe ushort[] BuildRangesToArray(
|
||||
this ImFontGlyphRangesBuilderPtr builder,
|
||||
bool addFallbackCodepoints = true,
|
||||
bool addEllipsisCodepoints = true)
|
||||
{
|
||||
if (addFallbackCodepoints)
|
||||
builder.AddText(FontAtlasFactory.FallbackCodepoints);
|
||||
if (addEllipsisCodepoints)
|
||||
{
|
||||
builder.AddText(FontAtlasFactory.EllipsisCodepoints);
|
||||
builder.AddChar('.');
|
||||
}
|
||||
|
||||
builder.BuildRanges(out var vec);
|
||||
return new ReadOnlySpan<ushort>((void*)vec.Data, vec.Size).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="CreateImGuiRangesFrom(IEnumerable{UnicodeRange})"/>
|
||||
public static ushort[] CreateImGuiRangesFrom(params UnicodeRange[] ranges)
|
||||
=> CreateImGuiRangesFrom((IEnumerable<UnicodeRange>)ranges);
|
||||
|
||||
/// <summary>
|
||||
/// Creates glyph ranges from <see cref="UnicodeRange"/>.<br />
|
||||
/// Use values from <see cref="UnicodeRanges"/>.
|
||||
/// </summary>
|
||||
/// <param name="ranges">The unicode ranges.</param>
|
||||
/// <returns>The range array that can be used for <see cref="SafeFontConfig.GlyphRanges"/>.</returns>
|
||||
public static ushort[] CreateImGuiRangesFrom(IEnumerable<UnicodeRange> ranges) =>
|
||||
ranges
|
||||
.Where(x => x.FirstCodePoint <= ushort.MaxValue)
|
||||
.SelectMany(
|
||||
x => new[]
|
||||
{
|
||||
(ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue),
|
||||
(ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue),
|
||||
})
|
||||
.Append((ushort)0)
|
||||
.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether <paramref name="ptr"/> is empty.
|
||||
/// </summary>
|
||||
|
|
@ -415,7 +510,7 @@ public static class ImGuiHelpers
|
|||
public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether <paramref name="ptr"/> is not null and loaded.
|
||||
/// Determines whether <paramref name="ptr"/> is empty.
|
||||
/// </summary>
|
||||
/// <param name="ptr">The pointer.</param>
|
||||
/// <returns>Whether it is empty.</returns>
|
||||
|
|
@ -427,6 +522,27 @@ public static class ImGuiHelpers
|
|||
/// <param name="ptr">The pointer.</param>
|
||||
/// <returns>Whether it is empty.</returns>
|
||||
public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null;
|
||||
|
||||
/// <summary>
|
||||
/// If <paramref name="self"/> is default, then returns <paramref name="other"/>.
|
||||
/// </summary>
|
||||
/// <param name="self">The self.</param>
|
||||
/// <param name="other">The other.</param>
|
||||
/// <returns><paramref name="self"/> if it is not default; otherwise, <paramref name="other"/>.</returns>
|
||||
public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) =>
|
||||
self.NativePtr is null ? other : self;
|
||||
|
||||
/// <summary>
|
||||
/// Mark 4K page as used, after adding a codepoint to a font.
|
||||
/// </summary>
|
||||
/// <param name="font">The font.</param>
|
||||
/// <param name="codepoint">The codepoint.</param>
|
||||
internal static unsafe void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint)
|
||||
{
|
||||
// Mark 4K page as used
|
||||
var pageIndex = unchecked((ushort)(codepoint / 4096));
|
||||
font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the corresponding ImGui viewport ID for the given window handle.
|
||||
|
|
@ -448,6 +564,89 @@ public static class ImGuiHelpers
|
|||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to validate that <paramref name="fontPtr"/> is valid.
|
||||
/// </summary>
|
||||
/// <param name="fontPtr">The font pointer.</param>
|
||||
/// <returns>The exception, if any occurred during validation.</returns>
|
||||
internal static unsafe Exception? ValidateUnsafe(this ImFontPtr fontPtr)
|
||||
{
|
||||
try
|
||||
{
|
||||
var font = fontPtr.NativePtr;
|
||||
if (font is null)
|
||||
throw new NullReferenceException("The font is null.");
|
||||
|
||||
_ = Marshal.ReadIntPtr((nint)font);
|
||||
if (font->IndexedHotData.Data != 0)
|
||||
_ = Marshal.ReadIntPtr(font->IndexedHotData.Data);
|
||||
if (font->FrequentKerningPairs.Data != 0)
|
||||
_ = Marshal.ReadIntPtr(font->FrequentKerningPairs.Data);
|
||||
if (font->IndexLookup.Data != 0)
|
||||
_ = Marshal.ReadIntPtr(font->IndexLookup.Data);
|
||||
if (font->Glyphs.Data != 0)
|
||||
_ = Marshal.ReadIntPtr(font->Glyphs.Data);
|
||||
if (font->KerningPairs.Data != 0)
|
||||
_ = Marshal.ReadIntPtr(font->KerningPairs.Data);
|
||||
if (font->ConfigDataCount == 0 && font->ConfigData is not null)
|
||||
throw new InvalidOperationException("ConfigDataCount == 0 but ConfigData is not null?");
|
||||
if (font->ConfigDataCount != 0 && font->ConfigData is null)
|
||||
throw new InvalidOperationException("ConfigDataCount != 0 but ConfigData is null?");
|
||||
if (font->ConfigData is not null)
|
||||
_ = Marshal.ReadIntPtr((nint)font->ConfigData);
|
||||
if (font->FallbackGlyph is not null
|
||||
&& ((nint)font->FallbackGlyph < font->Glyphs.Data || (nint)font->FallbackGlyph >= font->Glyphs.Data))
|
||||
throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data");
|
||||
if (font->FallbackHotData is not null
|
||||
&& ((nint)font->FallbackHotData < font->IndexedHotData.Data
|
||||
|| (nint)font->FallbackHotData >= font->IndexedHotData.Data))
|
||||
throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data");
|
||||
if (font->ContainerAtlas is not null)
|
||||
_ = Marshal.ReadIntPtr((nint)font->ContainerAtlas);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return e;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the fallback char of <paramref name="font"/>.
|
||||
/// </summary>
|
||||
/// <param name="font">The font.</param>
|
||||
/// <param name="c">The fallback character.</param>
|
||||
internal static unsafe void UpdateFallbackChar(this ImFontPtr font, char c)
|
||||
{
|
||||
font.FallbackChar = c;
|
||||
font.NativePtr->FallbackHotData =
|
||||
(ImFontGlyphHotData*)((ImFontGlyphHotDataReal*)font.IndexedHotData.Data + font.FallbackChar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the supplied codepoint is inside the given range,
|
||||
/// in format of <see cref="ImFontConfig.GlyphRanges"/>.
|
||||
/// </summary>
|
||||
/// <param name="codepoint">The codepoint.</param>
|
||||
/// <param name="rangePtr">The ranges.</param>
|
||||
/// <returns>Whether it is the case.</returns>
|
||||
internal static unsafe bool IsCodepointInSuppliedGlyphRangesUnsafe(int codepoint, ushort* rangePtr)
|
||||
{
|
||||
if (codepoint is <= 0 or >= ushort.MaxValue)
|
||||
return false;
|
||||
|
||||
while (*rangePtr != 0)
|
||||
{
|
||||
var from = *rangePtr++;
|
||||
var to = *rangePtr++;
|
||||
if (from <= codepoint && codepoint <= to)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get data needed for each new frame.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ public class ModuleLog
|
|||
/// <param name="messageTemplate">The message template.</param>
|
||||
/// <param name="values">Values to log.</param>
|
||||
[MessageTemplateFormatMethod("messageTemplate")]
|
||||
public void Verbose(string messageTemplate, params object[] values)
|
||||
public void Verbose(string messageTemplate, params object?[] values)
|
||||
=> this.WriteLog(LogEventLevel.Verbose, messageTemplate, null, values);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -43,7 +43,7 @@ public class ModuleLog
|
|||
/// <param name="messageTemplate">The message template.</param>
|
||||
/// <param name="values">Values to log.</param>
|
||||
[MessageTemplateFormatMethod("messageTemplate")]
|
||||
public void Verbose(Exception exception, string messageTemplate, params object[] values)
|
||||
public void Verbose(Exception? exception, string messageTemplate, params object?[] values)
|
||||
=> this.WriteLog(LogEventLevel.Verbose, messageTemplate, exception, values);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -52,7 +52,7 @@ public class ModuleLog
|
|||
/// <param name="messageTemplate">The message template.</param>
|
||||
/// <param name="values">Values to log.</param>
|
||||
[MessageTemplateFormatMethod("messageTemplate")]
|
||||
public void Debug(string messageTemplate, params object[] values)
|
||||
public void Debug(string messageTemplate, params object?[] values)
|
||||
=> this.WriteLog(LogEventLevel.Debug, messageTemplate, null, values);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -62,7 +62,7 @@ public class ModuleLog
|
|||
/// <param name="messageTemplate">The message template.</param>
|
||||
/// <param name="values">Values to log.</param>
|
||||
[MessageTemplateFormatMethod("messageTemplate")]
|
||||
public void Debug(Exception exception, string messageTemplate, params object[] values)
|
||||
public void Debug(Exception? exception, string messageTemplate, params object?[] values)
|
||||
=> this.WriteLog(LogEventLevel.Debug, messageTemplate, exception, values);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -71,7 +71,7 @@ public class ModuleLog
|
|||
/// <param name="messageTemplate">The message template.</param>
|
||||
/// <param name="values">Values to log.</param>
|
||||
[MessageTemplateFormatMethod("messageTemplate")]
|
||||
public void Information(string messageTemplate, params object[] values)
|
||||
public void Information(string messageTemplate, params object?[] values)
|
||||
=> this.WriteLog(LogEventLevel.Information, messageTemplate, null, values);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -81,7 +81,7 @@ public class ModuleLog
|
|||
/// <param name="messageTemplate">The message template.</param>
|
||||
/// <param name="values">Values to log.</param>
|
||||
[MessageTemplateFormatMethod("messageTemplate")]
|
||||
public void Information(Exception exception, string messageTemplate, params object[] values)
|
||||
public void Information(Exception? exception, string messageTemplate, params object?[] values)
|
||||
=> this.WriteLog(LogEventLevel.Information, messageTemplate, exception, values);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -90,7 +90,7 @@ public class ModuleLog
|
|||
/// <param name="messageTemplate">The message template.</param>
|
||||
/// <param name="values">Values to log.</param>
|
||||
[MessageTemplateFormatMethod("messageTemplate")]
|
||||
public void Warning(string messageTemplate, params object[] values)
|
||||
public void Warning(string messageTemplate, params object?[] values)
|
||||
=> this.WriteLog(LogEventLevel.Warning, messageTemplate, null, values);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -100,7 +100,7 @@ public class ModuleLog
|
|||
/// <param name="messageTemplate">The message template.</param>
|
||||
/// <param name="values">Values to log.</param>
|
||||
[MessageTemplateFormatMethod("messageTemplate")]
|
||||
public void Warning(Exception exception, string messageTemplate, params object[] values)
|
||||
public void Warning(Exception? exception, string messageTemplate, params object?[] values)
|
||||
=> this.WriteLog(LogEventLevel.Warning, messageTemplate, exception, values);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -109,7 +109,7 @@ public class ModuleLog
|
|||
/// <param name="messageTemplate">The message template.</param>
|
||||
/// <param name="values">Values to log.</param>
|
||||
[MessageTemplateFormatMethod("messageTemplate")]
|
||||
public void Error(string messageTemplate, params object[] values)
|
||||
public void Error(string messageTemplate, params object?[] values)
|
||||
=> this.WriteLog(LogEventLevel.Error, messageTemplate, null, values);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -119,7 +119,7 @@ public class ModuleLog
|
|||
/// <param name="messageTemplate">The message template.</param>
|
||||
/// <param name="values">Values to log.</param>
|
||||
[MessageTemplateFormatMethod("messageTemplate")]
|
||||
public void Error(Exception? exception, string messageTemplate, params object[] values)
|
||||
public void Error(Exception? exception, string messageTemplate, params object?[] values)
|
||||
=> this.WriteLog(LogEventLevel.Error, messageTemplate, exception, values);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -128,7 +128,7 @@ public class ModuleLog
|
|||
/// <param name="messageTemplate">The message template.</param>
|
||||
/// <param name="values">Values to log.</param>
|
||||
[MessageTemplateFormatMethod("messageTemplate")]
|
||||
public void Fatal(string messageTemplate, params object[] values)
|
||||
public void Fatal(string messageTemplate, params object?[] values)
|
||||
=> this.WriteLog(LogEventLevel.Fatal, messageTemplate, null, values);
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -138,12 +138,12 @@ public class ModuleLog
|
|||
/// <param name="messageTemplate">The message template.</param>
|
||||
/// <param name="values">Values to log.</param>
|
||||
[MessageTemplateFormatMethod("messageTemplate")]
|
||||
public void Fatal(Exception exception, string messageTemplate, params object[] values)
|
||||
public void Fatal(Exception? exception, string messageTemplate, params object?[] values)
|
||||
=> this.WriteLog(LogEventLevel.Fatal, messageTemplate, exception, values);
|
||||
|
||||
[MessageTemplateFormatMethod("messageTemplate")]
|
||||
private void WriteLog(
|
||||
LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values)
|
||||
LogEventLevel level, string messageTemplate, Exception? exception = null, params object?[] values)
|
||||
{
|
||||
// FIXME: Eventually, the `pluginName` tag should be removed from here and moved over to the actual log
|
||||
// formatter.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
using System.Reflection;
|
||||
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility;
|
||||
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
|
||||
|
|
@ -14,6 +16,7 @@ namespace Dalamud.Logging;
|
|||
/// move over as soon as reasonably possible for performance reasons.
|
||||
/// </remarks>
|
||||
[Obsolete("Static PluginLog will be removed in API 10. Developers should use IPluginLog.")]
|
||||
[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)]
|
||||
public static class PluginLog
|
||||
{
|
||||
#region "Log" prefixed Serilog style methods
|
||||
|
|
|
|||
|
|
@ -666,6 +666,15 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
this.PluginsReady = true;
|
||||
this.NotifyinstalledPluginsListChanged();
|
||||
sigScanner.Save();
|
||||
|
||||
try
|
||||
{
|
||||
this.ParanoiaValidatePluginsAndProfiles();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Plugin and profile validation failed!");
|
||||
}
|
||||
},
|
||||
tokenSource.Token);
|
||||
}
|
||||
|
|
@ -1088,7 +1097,7 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
{
|
||||
try
|
||||
{
|
||||
this.PluginConfigs.Delete(plugin.Name);
|
||||
this.PluginConfigs.Delete(plugin.Manifest.InternalName);
|
||||
break;
|
||||
}
|
||||
catch (IOException)
|
||||
|
|
@ -1259,6 +1268,30 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if there are any inconsistencies with our plugins, their IDs, and our profiles.
|
||||
/// </summary>
|
||||
private void ParanoiaValidatePluginsAndProfiles()
|
||||
{
|
||||
var seenIds = new List<Guid>();
|
||||
|
||||
foreach (var installedPlugin in this.InstalledPlugins)
|
||||
{
|
||||
if (installedPlugin.Manifest.WorkingPluginId == Guid.Empty)
|
||||
throw new Exception($"{(installedPlugin is LocalDevPlugin ? "DevPlugin" : "Plugin")} '{installedPlugin.Manifest.InternalName}' has an empty WorkingPluginId.");
|
||||
|
||||
if (seenIds.Contains(installedPlugin.Manifest.WorkingPluginId))
|
||||
{
|
||||
throw new Exception(
|
||||
$"{(installedPlugin is LocalDevPlugin ? "DevPlugin" : "Plugin")} '{installedPlugin.Manifest.InternalName}' has a duplicate WorkingPluginId '{installedPlugin.Manifest.WorkingPluginId}'");
|
||||
}
|
||||
|
||||
seenIds.Add(installedPlugin.Manifest.WorkingPluginId);
|
||||
}
|
||||
|
||||
this.profileManager.ParanoiaValidateProfiles();
|
||||
}
|
||||
|
||||
private async Task<Stream> DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting)
|
||||
{
|
||||
var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall;
|
||||
|
|
@ -1300,7 +1333,7 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
try
|
||||
{
|
||||
// We don't need to apply, it doesn't matter
|
||||
await this.profileManager.DefaultProfile.RemoveAsync(repoManifest.InternalName, false);
|
||||
await this.profileManager.DefaultProfile.RemoveByInternalNameAsync(repoManifest.InternalName, false);
|
||||
}
|
||||
catch (ProfileOperationException)
|
||||
{
|
||||
|
|
@ -1448,73 +1481,98 @@ internal partial class PluginManager : IDisposable, IServiceType
|
|||
if (isDev)
|
||||
{
|
||||
Log.Information($"Loading dev plugin {name}");
|
||||
var devPlugin = new LocalDevPlugin(dllFile, manifest);
|
||||
loadPlugin &= !isBoot;
|
||||
|
||||
var probablyInternalNameForThisPurpose = manifest?.InternalName ?? dllFile.Name;
|
||||
|
||||
var wantsInDefaultProfile =
|
||||
this.profileManager.DefaultProfile.WantsPlugin(probablyInternalNameForThisPurpose);
|
||||
if (wantsInDefaultProfile == null)
|
||||
{
|
||||
// We don't know about this plugin, so we don't want to do anything here.
|
||||
// The code below will take care of it and add it with the default value.
|
||||
}
|
||||
else if (wantsInDefaultProfile == false && devPlugin.StartOnBoot)
|
||||
{
|
||||
// We didn't want this plugin, and StartOnBoot is on. That means we don't want it and it should stay off until manually enabled.
|
||||
Log.Verbose("DevPlugin {Name} disabled and StartOnBoot => disable", probablyInternalNameForThisPurpose);
|
||||
await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false);
|
||||
loadPlugin = false;
|
||||
}
|
||||
else if (wantsInDefaultProfile == true && devPlugin.StartOnBoot)
|
||||
{
|
||||
// We wanted this plugin, and StartOnBoot is on. That means we actually do want it.
|
||||
Log.Verbose("DevPlugin {Name} enabled and StartOnBoot => enable", probablyInternalNameForThisPurpose);
|
||||
await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, true, false);
|
||||
loadPlugin = !doNotLoad;
|
||||
}
|
||||
else if (wantsInDefaultProfile == true && !devPlugin.StartOnBoot)
|
||||
{
|
||||
// We wanted this plugin, but StartOnBoot is off. This means we don't want it anymore.
|
||||
Log.Verbose("DevPlugin {Name} enabled and !StartOnBoot => disable", probablyInternalNameForThisPurpose);
|
||||
await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false);
|
||||
loadPlugin = false;
|
||||
}
|
||||
else if (wantsInDefaultProfile == false && !devPlugin.StartOnBoot)
|
||||
{
|
||||
// We didn't want this plugin, and StartOnBoot is off. We don't want it.
|
||||
Log.Verbose("DevPlugin {Name} disabled and !StartOnBoot => disable", probablyInternalNameForThisPurpose);
|
||||
await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false);
|
||||
loadPlugin = false;
|
||||
}
|
||||
|
||||
plugin = devPlugin;
|
||||
plugin = new LocalDevPlugin(dllFile, manifest);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Information($"Loading plugin {name}");
|
||||
plugin = new LocalPlugin(dllFile, manifest);
|
||||
}
|
||||
|
||||
// Perform a migration from InternalName to GUIDs. The plugin should definitely have a GUID here.
|
||||
// This will also happen if you are installing a plugin with the installer, and that's intended!
|
||||
// It means that, if you have a profile which has unsatisfied plugins, installing a matching plugin will
|
||||
// enter it into the profiles it can match.
|
||||
if (plugin.Manifest.WorkingPluginId == Guid.Empty)
|
||||
throw new Exception("Plugin should have a WorkingPluginId at this point");
|
||||
this.profileManager.MigrateProfilesToGuidsForPlugin(plugin.Manifest.InternalName, plugin.Manifest.WorkingPluginId);
|
||||
|
||||
var wantedByAnyProfile = false;
|
||||
|
||||
// Now, if this is a devPlugin, figure out if we want to load it
|
||||
if (isDev)
|
||||
{
|
||||
var devPlugin = (LocalDevPlugin)plugin;
|
||||
loadPlugin &= !isBoot;
|
||||
|
||||
var wantsInDefaultProfile =
|
||||
this.profileManager.DefaultProfile.WantsPlugin(plugin.Manifest.WorkingPluginId);
|
||||
if (wantsInDefaultProfile == null)
|
||||
{
|
||||
// We don't know about this plugin, so we don't want to do anything here.
|
||||
// The code below will take care of it and add it with the default value.
|
||||
Log.Verbose("DevPlugin {Name} not wanted in default plugin", plugin.Manifest.InternalName);
|
||||
|
||||
// Check if any profile wants this plugin. We need to do this here, since we want to allow loading a dev plugin if a non-default profile wants it active.
|
||||
// Note that this will not add the plugin to the default profile. That's done below in any other case.
|
||||
wantedByAnyProfile = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false);
|
||||
|
||||
// If it is wanted by any other profile, we do want to load it.
|
||||
if (wantedByAnyProfile)
|
||||
loadPlugin = true;
|
||||
}
|
||||
else if (wantsInDefaultProfile == false && devPlugin.StartOnBoot)
|
||||
{
|
||||
// We didn't want this plugin, and StartOnBoot is on. That means we don't want it and it should stay off until manually enabled.
|
||||
Log.Verbose("DevPlugin {Name} disabled and StartOnBoot => disable", plugin.Manifest.InternalName);
|
||||
await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false);
|
||||
loadPlugin = false;
|
||||
}
|
||||
else if (wantsInDefaultProfile == true && devPlugin.StartOnBoot)
|
||||
{
|
||||
// We wanted this plugin, and StartOnBoot is on. That means we actually do want it.
|
||||
Log.Verbose("DevPlugin {Name} enabled and StartOnBoot => enable", plugin.Manifest.InternalName);
|
||||
await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false);
|
||||
loadPlugin = !doNotLoad;
|
||||
}
|
||||
else if (wantsInDefaultProfile == true && !devPlugin.StartOnBoot)
|
||||
{
|
||||
// We wanted this plugin, but StartOnBoot is off. This means we don't want it anymore.
|
||||
Log.Verbose("DevPlugin {Name} enabled and !StartOnBoot => disable", plugin.Manifest.InternalName);
|
||||
await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false);
|
||||
loadPlugin = false;
|
||||
}
|
||||
else if (wantsInDefaultProfile == false && !devPlugin.StartOnBoot)
|
||||
{
|
||||
// We didn't want this plugin, and StartOnBoot is off. We don't want it.
|
||||
Log.Verbose("DevPlugin {Name} disabled and !StartOnBoot => disable", plugin.Manifest.InternalName);
|
||||
await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false);
|
||||
loadPlugin = false;
|
||||
}
|
||||
|
||||
plugin = devPlugin;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
var defaultState = manifest?.Disabled != true && loadPlugin;
|
||||
#pragma warning restore CS0618
|
||||
|
||||
// Need to do this here, so plugins that don't load are still added to the default profile
|
||||
var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.InternalName, defaultState);
|
||||
|
||||
|
||||
// Plugins that aren't in any profile will be added to the default profile with this call.
|
||||
// We are skipping a double-lookup for dev plugins that are wanted by non-default profiles, as noted above.
|
||||
wantedByAnyProfile = wantedByAnyProfile || await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState);
|
||||
Log.Information("{Name} defaultState: {State} wantedByAnyProfile: {WantedByAny} loadPlugin: {LoadPlugin}", plugin.Manifest.InternalName, defaultState, wantedByAnyProfile, loadPlugin);
|
||||
|
||||
if (loadPlugin)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (wantToLoad && !plugin.IsOrphaned)
|
||||
if (wantedByAnyProfile && !plugin.IsOrphaned)
|
||||
{
|
||||
await plugin.LoadAsync(reason);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Verbose($"{name} not loaded, wantToLoad:{wantToLoad} orphaned:{plugin.IsOrphaned}");
|
||||
Log.Verbose($"{name} not loaded, wantToLoad:{wantedByAnyProfile} orphaned:{plugin.IsOrphaned}");
|
||||
}
|
||||
}
|
||||
catch (InvalidPluginException)
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ internal class Profile
|
|||
/// Gets all plugins declared in this profile.
|
||||
/// </summary>
|
||||
public IEnumerable<ProfilePluginEntry> Plugins =>
|
||||
this.modelV1.Plugins.Select(x => new ProfilePluginEntry(x.InternalName, x.IsEnabled));
|
||||
this.modelV1.Plugins.Select(x => new ProfilePluginEntry(x.InternalName, x.WorkingPluginId, x.IsEnabled));
|
||||
|
||||
/// <summary>
|
||||
/// Gets this profile's underlying model.
|
||||
|
|
@ -142,13 +142,13 @@ internal class Profile
|
|||
/// <summary>
|
||||
/// Check if this profile contains a specific plugin, and if it is enabled.
|
||||
/// </summary>
|
||||
/// <param name="internalName">The internal name of the plugin.</param>
|
||||
/// <param name="workingPluginId">The ID of the plugin.</param>
|
||||
/// <returns>Null if this profile does not declare the plugin, true if the profile declares the plugin and wants it enabled, false if the profile declares the plugin and does not want it enabled.</returns>
|
||||
public bool? WantsPlugin(string internalName)
|
||||
public bool? WantsPlugin(Guid workingPluginId)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName);
|
||||
var entry = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId);
|
||||
return entry?.IsEnabled;
|
||||
}
|
||||
}
|
||||
|
|
@ -157,17 +157,18 @@ internal class Profile
|
|||
/// Add a plugin to this profile with the desired state, or change the state of a plugin in this profile.
|
||||
/// This will block until all states have been applied.
|
||||
/// </summary>
|
||||
/// <param name="internalName">The internal name of the plugin.</param>
|
||||
/// <param name="workingPluginId">The ID of the plugin.</param>
|
||||
/// <param name="internalName">The internal name of the plugin, if available.</param>
|
||||
/// <param name="state">Whether or not the plugin should be enabled.</param>
|
||||
/// <param name="apply">Whether or not the current state should immediately be applied.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public async Task AddOrUpdateAsync(string internalName, bool state, bool apply = true)
|
||||
public async Task AddOrUpdateAsync(Guid workingPluginId, string? internalName, bool state, bool apply = true)
|
||||
{
|
||||
Debug.Assert(!internalName.IsNullOrEmpty(), "!internalName.IsNullOrEmpty()");
|
||||
|
||||
Debug.Assert(workingPluginId != Guid.Empty, "Trying to add plugin with empty guid");
|
||||
|
||||
lock (this)
|
||||
{
|
||||
var existing = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName);
|
||||
var existing = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId);
|
||||
if (existing != null)
|
||||
{
|
||||
existing.IsEnabled = state;
|
||||
|
|
@ -177,15 +178,55 @@ internal class Profile
|
|||
this.modelV1.Plugins.Add(new ProfileModelV1.ProfileModelV1Plugin
|
||||
{
|
||||
InternalName = internalName,
|
||||
WorkingPluginId = workingPluginId,
|
||||
IsEnabled = state,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// We need to remove this plugin from the default profile, if it declares it.
|
||||
if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(internalName) != null)
|
||||
if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(workingPluginId) != null)
|
||||
{
|
||||
await this.manager.DefaultProfile.RemoveAsync(internalName, false);
|
||||
await this.manager.DefaultProfile.RemoveAsync(workingPluginId, false);
|
||||
}
|
||||
|
||||
Service<DalamudConfiguration>.Get().QueueSave();
|
||||
|
||||
if (apply)
|
||||
await this.manager.ApplyAllWantStatesAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a plugin from this profile.
|
||||
/// This will block until all states have been applied.
|
||||
/// </summary>
|
||||
/// <param name="workingPluginId">The ID of the plugin.</param>
|
||||
/// <param name="apply">Whether or not the current state should immediately be applied.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public async Task RemoveAsync(Guid workingPluginId, bool apply = true)
|
||||
{
|
||||
ProfileModelV1.ProfileModelV1Plugin entry;
|
||||
lock (this)
|
||||
{
|
||||
entry = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId);
|
||||
if (entry == null)
|
||||
throw new PluginNotFoundException(workingPluginId);
|
||||
|
||||
if (!this.modelV1.Plugins.Remove(entry))
|
||||
throw new Exception("Couldn't remove plugin from model collection");
|
||||
}
|
||||
|
||||
// We need to add this plugin back to the default profile, if we were the last profile to have it.
|
||||
if (!this.manager.IsInAnyProfile(workingPluginId))
|
||||
{
|
||||
if (!this.IsDefaultProfile)
|
||||
{
|
||||
await this.manager.DefaultProfile.AddOrUpdateAsync(workingPluginId, entry.InternalName, this.IsEnabled && entry.IsEnabled, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PluginNotInDefaultProfileException(workingPluginId.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
Service<DalamudConfiguration>.Get().QueueSave();
|
||||
|
|
@ -201,36 +242,50 @@ internal class Profile
|
|||
/// <param name="internalName">The internal name of the plugin.</param>
|
||||
/// <param name="apply">Whether or not the current state should immediately be applied.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
public async Task RemoveAsync(string internalName, bool apply = true)
|
||||
public async Task RemoveByInternalNameAsync(string internalName, bool apply = true)
|
||||
{
|
||||
ProfileModelV1.ProfileModelV1Plugin entry;
|
||||
Guid? pluginToRemove = null;
|
||||
lock (this)
|
||||
{
|
||||
entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName);
|
||||
if (entry == null)
|
||||
throw new PluginNotFoundException(internalName);
|
||||
|
||||
if (!this.modelV1.Plugins.Remove(entry))
|
||||
throw new Exception("Couldn't remove plugin from model collection");
|
||||
foreach (var plugin in this.Plugins)
|
||||
{
|
||||
if (plugin.InternalName.Equals(internalName, StringComparison.Ordinal))
|
||||
{
|
||||
pluginToRemove = plugin.WorkingPluginId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to add this plugin back to the default profile, if we were the last profile to have it.
|
||||
if (!this.manager.IsInAnyProfile(internalName))
|
||||
await this.RemoveAsync(pluginToRemove ?? throw new PluginNotFoundException(internalName), apply);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function tries to migrate all plugins with this internalName which do not have
|
||||
/// a GUID to the specified GUID.
|
||||
/// This is best-effort and will probably work well for anyone that only uses regular plugins.
|
||||
/// </summary>
|
||||
/// <param name="internalName">InternalName of the plugin to migrate.</param>
|
||||
/// <param name="newGuid">Guid to use.</param>
|
||||
public void MigrateProfilesToGuidsForPlugin(string internalName, Guid newGuid)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (!this.IsDefaultProfile)
|
||||
foreach (var plugin in this.modelV1.Plugins)
|
||||
{
|
||||
await this.manager.DefaultProfile.AddOrUpdateAsync(internalName, this.IsEnabled && entry.IsEnabled, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PluginNotInDefaultProfileException(internalName);
|
||||
// TODO: What should happen if a profile has a GUID locked in, but the plugin
|
||||
// is not installed anymore? That probably means that the user uninstalled the plugin
|
||||
// and is now reinstalling it. We should still satisfy that and update the ID.
|
||||
|
||||
if (plugin.InternalName == internalName && plugin.WorkingPluginId == Guid.Empty)
|
||||
{
|
||||
plugin.WorkingPluginId = newGuid;
|
||||
Log.Information("Migrated profile {Profile} plugin {Name} to guid {Guid}", this, internalName, newGuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Service<DalamudConfiguration>.Get().QueueSave();
|
||||
|
||||
if (apply)
|
||||
await this.manager.ApplyAllWantStatesAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -280,4 +335,13 @@ internal sealed class PluginNotFoundException : ProfileOperationException
|
|||
: base($"The plugin '{internalName}' was not found in the profile")
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PluginNotFoundException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="workingPluginId">The ID of the plugin causing the error.</param>
|
||||
public PluginNotFoundException(Guid workingPluginId)
|
||||
: base($"The plugin '{workingPluginId}' was not found in the profile")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,11 +69,12 @@ internal class ProfileManager : IServiceType
|
|||
/// <summary>
|
||||
/// Check if any enabled profile wants a specific plugin enabled.
|
||||
/// </summary>
|
||||
/// <param name="internalName">The internal name of the plugin.</param>
|
||||
/// <param name="workingPluginId">The ID of the plugin.</param>
|
||||
/// <param name="internalName">The internal name of the plugin, if available.</param>
|
||||
/// <param name="defaultState">The state the plugin shall be in, if it needs to be added.</param>
|
||||
/// <param name="addIfNotDeclared">Whether or not the plugin should be added to the default preset, if it's not present in any preset.</param>
|
||||
/// <returns>Whether or not the plugin shall be enabled.</returns>
|
||||
public async Task<bool> GetWantStateAsync(string internalName, bool defaultState, bool addIfNotDeclared = true)
|
||||
public async Task<bool> GetWantStateAsync(Guid workingPluginId, string? internalName, bool defaultState, bool addIfNotDeclared = true)
|
||||
{
|
||||
var want = false;
|
||||
var wasInAnyProfile = false;
|
||||
|
|
@ -82,7 +83,7 @@ internal class ProfileManager : IServiceType
|
|||
{
|
||||
foreach (var profile in this.profiles)
|
||||
{
|
||||
var state = profile.WantsPlugin(internalName);
|
||||
var state = profile.WantsPlugin(workingPluginId);
|
||||
if (state.HasValue)
|
||||
{
|
||||
want = want || (profile.IsEnabled && state.Value);
|
||||
|
|
@ -93,8 +94,8 @@ internal class ProfileManager : IServiceType
|
|||
|
||||
if (!wasInAnyProfile && addIfNotDeclared)
|
||||
{
|
||||
Log.Warning("{Name} was not in any profile, adding to default with {Default}", internalName, defaultState);
|
||||
await this.DefaultProfile.AddOrUpdateAsync(internalName, defaultState, false);
|
||||
Log.Warning("'{Guid}'('{InternalName}') was not in any profile, adding to default with {Default}", workingPluginId, internalName, defaultState);
|
||||
await this.DefaultProfile.AddOrUpdateAsync(workingPluginId, internalName, defaultState, false);
|
||||
|
||||
return defaultState;
|
||||
}
|
||||
|
|
@ -105,22 +106,22 @@ internal class ProfileManager : IServiceType
|
|||
/// <summary>
|
||||
/// Check whether a plugin is declared in any profile.
|
||||
/// </summary>
|
||||
/// <param name="internalName">The internal name of the plugin.</param>
|
||||
/// <param name="workingPluginId">The ID of the plugin.</param>
|
||||
/// <returns>Whether or not the plugin is in any profile.</returns>
|
||||
public bool IsInAnyProfile(string internalName)
|
||||
public bool IsInAnyProfile(Guid workingPluginId)
|
||||
{
|
||||
lock (this.profiles)
|
||||
return this.profiles.Any(x => x.WantsPlugin(internalName) != null);
|
||||
return this.profiles.Any(x => x.WantsPlugin(workingPluginId) != null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether a plugin is only in the default profile.
|
||||
/// A plugin can never be in the default profile if it is in any other profile.
|
||||
/// </summary>
|
||||
/// <param name="internalName">The internal name of the plugin.</param>
|
||||
/// <param name="workingPluginId">The ID of the plugin.</param>
|
||||
/// <returns>Whether or not the plugin is in the default profile.</returns>
|
||||
public bool IsInDefaultProfile(string internalName)
|
||||
=> this.DefaultProfile.WantsPlugin(internalName) != null;
|
||||
public bool IsInDefaultProfile(Guid workingPluginId)
|
||||
=> this.DefaultProfile.WantsPlugin(workingPluginId) != null;
|
||||
|
||||
/// <summary>
|
||||
/// Add a new profile.
|
||||
|
|
@ -151,7 +152,7 @@ internal class ProfileManager : IServiceType
|
|||
/// <returns>The newly cloned profile.</returns>
|
||||
public Profile CloneProfile(Profile toClone)
|
||||
{
|
||||
var newProfile = this.ImportProfile(toClone.Model.Serialize());
|
||||
var newProfile = this.ImportProfile(toClone.Model.SerializeForShare());
|
||||
if (newProfile == null)
|
||||
throw new Exception("New profile was null while cloning");
|
||||
|
||||
|
|
@ -172,7 +173,27 @@ internal class ProfileManager : IServiceType
|
|||
newModel.Guid = Guid.NewGuid();
|
||||
newModel.Name = this.GenerateUniqueProfileName(newModel.Name.IsNullOrEmpty() ? "Unknown Collection" : newModel.Name);
|
||||
if (newModel is ProfileModelV1 modelV1)
|
||||
{
|
||||
// Disable it
|
||||
modelV1.IsEnabled = false;
|
||||
|
||||
// Try to find matching plugins for all plugins in the profile
|
||||
var pm = Service<PluginManager>.Get();
|
||||
foreach (var plugin in modelV1.Plugins)
|
||||
{
|
||||
var installedPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == plugin.InternalName);
|
||||
if (installedPlugin != null)
|
||||
{
|
||||
Log.Information("Satisfying plugin {InternalName} for profile {Name} with {Guid}", plugin.InternalName, newModel.Name, installedPlugin.Manifest.WorkingPluginId);
|
||||
plugin.WorkingPluginId = installedPlugin.Manifest.WorkingPluginId;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("Couldn't find plugin {InternalName} for profile {Name}", plugin.InternalName, newModel.Name);
|
||||
plugin.WorkingPluginId = Guid.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.config.SavedProfiles!.Add(newModel);
|
||||
this.config.QueueSave();
|
||||
|
|
@ -196,19 +217,18 @@ internal class ProfileManager : IServiceType
|
|||
this.isBusy = true;
|
||||
Log.Information("Getting want states...");
|
||||
|
||||
List<string> wantActive;
|
||||
List<ProfilePluginEntry> wantActive;
|
||||
lock (this.profiles)
|
||||
{
|
||||
wantActive = this.profiles
|
||||
.Where(x => x.IsEnabled)
|
||||
.SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled)
|
||||
.Select(plugin => plugin.InternalName))
|
||||
.SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled))
|
||||
.Distinct().ToList();
|
||||
}
|
||||
|
||||
foreach (var internalName in wantActive)
|
||||
foreach (var profilePluginEntry in wantActive)
|
||||
{
|
||||
Log.Information("\t=> Want {Name}", internalName);
|
||||
Log.Information("\t=> Want {Name}({WorkingPluginId})", profilePluginEntry.InternalName, profilePluginEntry.WorkingPluginId);
|
||||
}
|
||||
|
||||
Log.Information("Applying want states...");
|
||||
|
|
@ -218,7 +238,7 @@ internal class ProfileManager : IServiceType
|
|||
var pm = Service<PluginManager>.Get();
|
||||
foreach (var installedPlugin in pm.InstalledPlugins)
|
||||
{
|
||||
var wantThis = wantActive.Contains(installedPlugin.Manifest.InternalName);
|
||||
var wantThis = wantActive.Any(x => x.WorkingPluginId == installedPlugin.Manifest.WorkingPluginId);
|
||||
switch (wantThis)
|
||||
{
|
||||
case true when !installedPlugin.IsLoaded:
|
||||
|
|
@ -267,7 +287,7 @@ internal class ProfileManager : IServiceType
|
|||
// We need to remove all plugins from the profile first, so that they are re-added to the default profile if needed
|
||||
foreach (var plugin in profile.Plugins.ToArray())
|
||||
{
|
||||
await profile.RemoveAsync(plugin.InternalName, false);
|
||||
await profile.RemoveAsync(plugin.WorkingPluginId, false);
|
||||
}
|
||||
|
||||
if (!this.config.SavedProfiles!.Remove(profile.Model))
|
||||
|
|
@ -279,6 +299,42 @@ internal class ProfileManager : IServiceType
|
|||
this.config.QueueSave();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This function tries to migrate all plugins with this internalName which do not have
|
||||
/// a GUID to the specified GUID.
|
||||
/// This is best-effort and will probably work well for anyone that only uses regular plugins.
|
||||
/// </summary>
|
||||
/// <param name="internalName">InternalName of the plugin to migrate.</param>
|
||||
/// <param name="newGuid">Guid to use.</param>
|
||||
public void MigrateProfilesToGuidsForPlugin(string internalName, Guid newGuid)
|
||||
{
|
||||
lock (this.profiles)
|
||||
{
|
||||
foreach (var profile in this.profiles)
|
||||
profile.MigrateProfilesToGuidsForPlugin(internalName, newGuid);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate profiles for errors.
|
||||
/// </summary>
|
||||
/// <exception cref="Exception">Thrown when a profile is not sane.</exception>
|
||||
public void ParanoiaValidateProfiles()
|
||||
{
|
||||
foreach (var profile in this.profiles)
|
||||
{
|
||||
var seenIds = new List<Guid>();
|
||||
|
||||
foreach (var pluginEntry in profile.Plugins)
|
||||
{
|
||||
if (seenIds.Contains(pluginEntry.WorkingPluginId))
|
||||
throw new Exception($"Plugin '{pluginEntry.WorkingPluginId}'('{pluginEntry.InternalName}') is twice in profile '{profile.Guid}'('{profile.Name}')");
|
||||
|
||||
seenIds.Add(pluginEntry.WorkingPluginId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateUniqueProfileName(string startingWith)
|
||||
{
|
||||
if (this.profiles.All(x => x.Name != startingWith))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
using Dalamud.Utility;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Dalamud.Plugin.Internal.Profiles;
|
||||
|
||||
|
|
@ -39,11 +41,11 @@ public abstract class ProfileModel
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialize this model into a string usable for sharing.
|
||||
/// Serialize this model into a string usable for sharing, without including GUIDs.
|
||||
/// </summary>
|
||||
/// <returns>The serialized representation of the model.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when an unsupported model is serialized.</exception>
|
||||
public string Serialize()
|
||||
public string SerializeForShare()
|
||||
{
|
||||
string prefix;
|
||||
switch (this)
|
||||
|
|
@ -55,6 +57,32 @@ public abstract class ProfileModel
|
|||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
return prefix + Convert.ToBase64String(Util.CompressString(JsonConvert.SerializeObject(this)));
|
||||
// HACK: Just filter the ID for now, we should split the sharing + saving model
|
||||
var serialized = JsonConvert.SerializeObject(this, new JsonSerializerSettings()
|
||||
{ ContractResolver = new IgnorePropertiesResolver(new[] { "WorkingPluginId" }) });
|
||||
|
||||
return prefix + Convert.ToBase64String(Util.CompressString(serialized));
|
||||
}
|
||||
|
||||
// Short helper class to ignore some properties from serialization
|
||||
private class IgnorePropertiesResolver : DefaultContractResolver
|
||||
{
|
||||
private readonly HashSet<string> ignoreProps;
|
||||
|
||||
public IgnorePropertiesResolver(IEnumerable<string> propNamesToIgnore)
|
||||
{
|
||||
this.ignoreProps = new HashSet<string>(propNamesToIgnore);
|
||||
}
|
||||
|
||||
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
|
||||
{
|
||||
var property = base.CreateProperty(member, memberSerialization);
|
||||
if (this.ignoreProps.Contains(property.PropertyName))
|
||||
{
|
||||
property.ShouldSerialize = _ => false;
|
||||
}
|
||||
|
||||
return property;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,11 @@ public class ProfileModelV1 : ProfileModel
|
|||
/// Gets or sets the internal name of the plugin.
|
||||
/// </summary>
|
||||
public string? InternalName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an ID uniquely identifying this specific instance of a plugin.
|
||||
/// </summary>
|
||||
public Guid WorkingPluginId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether or not this entry is enabled.
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ internal class ProfilePluginEntry
|
|||
/// Initializes a new instance of the <see cref="ProfilePluginEntry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="internalName">The internal name of the plugin.</param>
|
||||
/// <param name="workingPluginId">The ID of the plugin.</param>
|
||||
/// <param name="state">A value indicating whether or not this entry is enabled.</param>
|
||||
public ProfilePluginEntry(string internalName, bool state)
|
||||
public ProfilePluginEntry(string internalName, Guid workingPluginId, bool state)
|
||||
{
|
||||
this.InternalName = internalName;
|
||||
this.WorkingPluginId = workingPluginId;
|
||||
this.IsEnabled = state;
|
||||
}
|
||||
|
||||
|
|
@ -20,6 +22,11 @@ internal class ProfilePluginEntry
|
|||
/// Gets the internal name of the plugin.
|
||||
/// </summary>
|
||||
public string InternalName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an ID uniquely identifying this specific instance of a plugin.
|
||||
/// </summary>
|
||||
public Guid WorkingPluginId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether or not this entry is enabled.
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ internal class LocalPlugin : IDisposable
|
|||
/// INCLUDES the default profile.
|
||||
/// </summary>
|
||||
public bool IsWantedByAnyProfile =>
|
||||
Service<ProfileManager>.Get().GetWantStateAsync(this.manifest.InternalName, false, false).GetAwaiter().GetResult();
|
||||
Service<ProfileManager>.Get().GetWantStateAsync(this.manifest.WorkingPluginId, this.Manifest.InternalName, false, false).GetAwaiter().GetResult();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this plugin's API level is out of date.
|
||||
|
|
|
|||
|
|
@ -69,6 +69,14 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA
|
|||
.Select(x => x.ToContentDisposedTask()))
|
||||
.ContinueWith(_ => loadTimings.Dispose()),
|
||||
"Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available.");
|
||||
|
||||
Task.WhenAll(
|
||||
Enum.GetValues<DalamudAsset>()
|
||||
.Where(x => x is not DalamudAsset.Empty4X4)
|
||||
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is false)
|
||||
.Select(this.CreateStreamAsync)
|
||||
.Select(x => x.ToContentDisposedTask()))
|
||||
.ContinueWith(r => Log.Verbose($"Optional assets load state: {r}"));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Diagnostics.Contracts;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Diagnostics.Contracts;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
|
@ -64,8 +65,9 @@ internal interface IDalamudAssetManager
|
|||
/// </summary>
|
||||
/// <param name="asset">The texture asset.</param>
|
||||
/// <param name="defaultWrap">The default return value, if the asset is not ready for whatever reason.</param>
|
||||
/// <returns>The texture wrap.</returns>
|
||||
/// <returns>The texture wrap. Can be <c>null</c> only if <paramref name="defaultWrap"/> is <c>null</c>.</returns>
|
||||
[Pure]
|
||||
[return: NotNullIfNotNull(nameof(defaultWrap))]
|
||||
IDalamudTextureWrap? GetDalamudTextureWrap(DalamudAsset asset, IDalamudTextureWrap? defaultWrap);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
19
Dalamud/Utility/Api10ToDoAttribute.cs
Normal file
19
Dalamud/Utility/Api10ToDoAttribute.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
namespace Dalamud.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// Utility class for marking something to be changed for API 10, for ease of lookup.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.All, Inherited = false)]
|
||||
internal sealed class Api10ToDoAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks that this exists purely for making API 9 plugins work.
|
||||
/// </summary>
|
||||
public const string DeleteCompatBehavior = "Delete. This is for making API 9 plugins work.";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Api10ToDoAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="what">The explanation.</param>
|
||||
public Api10ToDoAttribute(string what) => _ = what;
|
||||
}
|
||||
77
Dalamud/Utility/IRefCountable.cs
Normal file
77
Dalamud/Utility/IRefCountable.cs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
|
||||
namespace Dalamud.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for reference counting.
|
||||
/// </summary>
|
||||
internal interface IRefCountable : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Result for <see cref="IRefCountable.AlterRefCount"/>.
|
||||
/// </summary>
|
||||
public enum RefCountResult
|
||||
{
|
||||
/// <summary>
|
||||
/// The object still has remaining references. No futher action should be done.
|
||||
/// </summary>
|
||||
StillAlive = 1,
|
||||
|
||||
/// <summary>
|
||||
/// The last reference to the object has been released. The object should be fully released.
|
||||
/// </summary>
|
||||
FinalRelease = 2,
|
||||
|
||||
/// <summary>
|
||||
/// The object already has been disposed. <see cref="ObjectDisposedException"/> may be thrown.
|
||||
/// </summary>
|
||||
AlreadyDisposed = 3,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a reference to this reference counted object.
|
||||
/// </summary>
|
||||
/// <returns>The new number of references.</returns>
|
||||
int AddRef();
|
||||
|
||||
/// <summary>
|
||||
/// Releases a reference from this reference counted object.<br />
|
||||
/// When all references are released, the object will be fully disposed.
|
||||
/// </summary>
|
||||
/// <returns>The new number of references.</returns>
|
||||
int Release();
|
||||
|
||||
/// <summary>
|
||||
/// Alias for <see cref="Release()"/>.
|
||||
/// </summary>
|
||||
void IDisposable.Dispose() => this.Release();
|
||||
|
||||
/// <summary>
|
||||
/// Alters <paramref name="refCount"/> by <paramref name="delta"/>.
|
||||
/// </summary>
|
||||
/// <param name="delta">The delta to the reference count.</param>
|
||||
/// <param name="refCount">The reference to the reference count.</param>
|
||||
/// <param name="newRefCount">The new reference count.</param>
|
||||
/// <returns>The followup action that should be done.</returns>
|
||||
public static RefCountResult AlterRefCount(int delta, ref int refCount, out int newRefCount)
|
||||
{
|
||||
Debug.Assert(delta is 1 or -1, "delta must be 1 or -1");
|
||||
|
||||
while (true)
|
||||
{
|
||||
var refCountCopy = refCount;
|
||||
if (refCountCopy <= 0)
|
||||
{
|
||||
newRefCount = refCountCopy;
|
||||
return RefCountResult.AlreadyDisposed;
|
||||
}
|
||||
|
||||
newRefCount = refCountCopy + delta;
|
||||
if (refCountCopy != Interlocked.CompareExchange(ref refCount, newRefCount, refCountCopy))
|
||||
continue;
|
||||
|
||||
return newRefCount == 0 ? RefCountResult.FinalRelease : RefCountResult.StillAlive;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
build.sh
Normal file → Executable file
2
build.sh
Normal file → Executable file
|
|
@ -59,4 +59,4 @@ fi
|
|||
echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)"
|
||||
|
||||
"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false /p:EnableWindowsTargeting=true -nologo -clp:NoSummary --verbosity quiet
|
||||
"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- /p:EnableWindowsTargeting=true "$@"
|
||||
"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit bbc4b994254d6913f51da3a20fad9bf4b8c986e5
|
||||
Subproject commit e3bd5910678683a718e68f0f940c88b08c24eba5
|
||||
Loading…
Add table
Add a link
Reference in a new issue