diff --git a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs
index 67ad3ee8f..34e07be8f 100644
--- a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs
+++ b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Numerics;
using Dalamud.Interface.Colors;
+using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Utility;
using Dalamud.Utility;
using ImGuiNET;
@@ -68,15 +69,22 @@ internal class NotificationManager : IServiceType
/// The title of the notification.
/// The type of the notification.
/// The time the notification should be displayed for.
- public void AddNotification(string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = NotifyDefaultDismiss)
+ /// The added notification.
+ public Notification AddNotification(
+ string content,
+ string? title = null,
+ NotificationType type = NotificationType.None,
+ uint msDelay = NotifyDefaultDismiss)
{
- this.notifications.Add(new Notification
+ var n = new Notification
{
Content = content,
Title = title,
NotificationType = type,
DurationMs = msDelay,
- });
+ };
+ this.notifications.Add(n);
+ return n;
}
///
@@ -97,6 +105,10 @@ internal class NotificationManager : IServiceType
continue;
}
+ using var pushedFont = tn.UseMonospaceFont
+ ? Service.Get().MonoFontHandle?.Push()
+ : null;
+
var opacity = tn.GetFadePercent();
var iconColor = tn.Color;
@@ -107,8 +119,12 @@ internal class NotificationManager : IServiceType
ImGuiHelpers.ForceNextWindowMainViewport();
ImGui.SetNextWindowBgAlpha(opacity);
ImGui.SetNextWindowPos(ImGuiHelpers.MainViewport.Pos + new Vector2(viewportSize.X - NotifyPaddingX, viewportSize.Y - NotifyPaddingY - height), ImGuiCond.Always, Vector2.One);
- ImGui.Begin(windowName, NotifyToastFlags);
+ if (tn.Actions.Count == 0)
+ ImGui.Begin(windowName, NotifyToastFlags);
+ else
+ ImGui.Begin(windowName, NotifyToastFlags & ~ImGuiWindowFlags.NoInputs);
+ ImGui.PushID(tn.NotificationId);
ImGui.PushTextWrapPos(viewportSize.X / 3.0f);
var wasTitleRendered = false;
@@ -162,10 +178,22 @@ internal class NotificationManager : IServiceType
ImGui.TextUnformatted(tn.Content);
}
+ foreach (var (caption, action) in tn.Actions)
+ {
+ if (ImGui.Button(caption))
+ action.InvokeSafely();
+ ImGui.SameLine();
+ }
+
+ // break ImGui.SameLine();
+ ImGui.TextUnformatted(string.Empty);
+
ImGui.PopStyleColor();
ImGui.PopTextWrapPos();
+ ImGui.PopID();
+
height += ImGui.GetWindowHeight() + NotifyPaddingMessageY;
ImGui.End();
@@ -177,6 +205,8 @@ internal class NotificationManager : IServiceType
///
internal class Notification
{
+ private static int notificationIdCounter;
+
///
/// Possible notification phases.
///
@@ -203,20 +233,40 @@ internal class NotificationManager : IServiceType
Expired,
}
+ ///
+ /// Gets the notification ID.
+ ///
+ internal int NotificationId { get; } = notificationIdCounter++;
+
///
/// Gets the type of the notification.
///
internal NotificationType NotificationType { get; init; }
///
- /// Gets the title of the notification.
+ /// Gets or sets a value indicating whether to force the use of monospace font.
///
- internal string? Title { get; init; }
+ internal bool UseMonospaceFont { get; set; }
///
- /// Gets the content of the notification.
+ /// Gets the action buttons to attach to this notification.
///
- internal string Content { get; init; }
+ internal List<(string Text, Action ClickCallback)> Actions { get; } = new();
+
+ ///
+ /// Gets or sets a value indicating whether this notification has been dismissed.
+ ///
+ internal bool Dismissed { get; set; }
+
+ ///
+ /// Gets or sets the title of the notification.
+ ///
+ internal string? Title { get; set; }
+
+ ///
+ /// Gets or sets the content of the notification.
+ ///
+ internal string? Content { get; set; }
///
/// Gets the duration of the notification in milliseconds.
@@ -283,7 +333,7 @@ internal class NotificationManager : IServiceType
{
var elapsed = (int)this.ElapsedTime.TotalMilliseconds;
- if (elapsed > NotifyFadeInOutTime + this.DurationMs + NotifyFadeInOutTime)
+ if (elapsed > NotifyFadeInOutTime + this.DurationMs + NotifyFadeInOutTime || this.Dismissed)
return Phase.Expired;
else if (elapsed > NotifyFadeInOutTime + this.DurationMs)
return Phase.FadeOut;
diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs
index d3bc976f2..021fc953f 100644
--- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs
+++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs
@@ -1,6 +1,7 @@
using System.Buffers;
using System.Collections.Generic;
using System.Collections.Immutable;
+using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -8,9 +9,13 @@ using System.Threading.Tasks;
using Dalamud.Configuration.Internal;
using Dalamud.Data;
using Dalamud.Game;
+using Dalamud.Game.ClientState;
+using Dalamud.Game.Gui;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Internal;
+using Dalamud.Interface.Internal.Notifications;
+using Dalamud.Plugin.Services;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
@@ -18,7 +23,11 @@ using ImGuiNET;
using ImGuiScene;
+using Lumina.Data;
using Lumina.Data.Files;
+using Lumina.Misc;
+
+using Newtonsoft.Json;
using SharpDX;
using SharpDX.Direct3D11;
@@ -33,9 +42,43 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals;
internal sealed partial class FontAtlasFactory
: IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable
{
+ private static readonly Dictionary KnownFontFileDataHashes = new()
+ {
+ ["common/font/AXIS_96.fdt"] = 1486212503,
+ ["common/font/AXIS_12.fdt"] = 1370045105,
+ ["common/font/AXIS_14.fdt"] = 645957730,
+ ["common/font/AXIS_18.fdt"] = 899094094,
+ ["common/font/AXIS_36.fdt"] = 2537048938,
+ ["common/font/Jupiter_16.fdt"] = 1642196098,
+ ["common/font/Jupiter_20.fdt"] = 3053628263,
+ ["common/font/Jupiter_23.fdt"] = 1536194944,
+ ["common/font/Jupiter_45.fdt"] = 3473589216,
+ ["common/font/Jupiter_46.fdt"] = 1370962087,
+ ["common/font/Jupiter_90.fdt"] = 3661420529,
+ ["common/font/Meidinger_16.fdt"] = 3700692128,
+ ["common/font/Meidinger_20.fdt"] = 441419856,
+ ["common/font/Meidinger_40.fdt"] = 203848091,
+ ["common/font/MiedingerMid_10.fdt"] = 499375313,
+ ["common/font/MiedingerMid_12.fdt"] = 1925552591,
+ ["common/font/MiedingerMid_14.fdt"] = 1919733827,
+ ["common/font/MiedingerMid_18.fdt"] = 1635778987,
+ ["common/font/MiedingerMid_36.fdt"] = 1190559864,
+ ["common/font/TrumpGothic_184.fdt"] = 973994576,
+ ["common/font/TrumpGothic_23.fdt"] = 1967289381,
+ ["common/font/TrumpGothic_34.fdt"] = 1777971886,
+ ["common/font/TrumpGothic_68.fdt"] = 1170173741,
+ ["common/font/font0.tex"] = 514269927,
+ ["common/font/font1.tex"] = 3616607606,
+ ["common/font/font2.tex"] = 4166651000,
+ ["common/font/font3.tex"] = 1264942640,
+ ["common/font/font4.tex"] = 3534300885,
+ ["common/font/font5.tex"] = 1041916216,
+ ["common/font/font6.tex"] = 1247097672,
+ };
+
private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new();
private readonly CancellationTokenSource cancellationTokenSource = new();
- private readonly IReadOnlyDictionary> fdtFiles;
+ private readonly IReadOnlyDictionary> fdtFiles;
private readonly IReadOnlyDictionary[]>> texFiles;
private readonly IReadOnlyDictionary> prebakedTextureWraps;
private readonly Task defaultGlyphRanges;
@@ -67,7 +110,7 @@ internal sealed partial class FontAtlasFactory
this.fdtFiles = gffasInfo.ToImmutableDictionary(
x => x.Font,
- x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data));
+ x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!));
var channelCountsTask = texPaths.ToImmutableDictionary(
x => x,
x => Task.WhenAll(
@@ -79,8 +122,8 @@ internal sealed partial class FontAtlasFactory
{
unsafe
{
- using var pin = file.AsMemory().Pin();
- var fdt = new FdtFileView(pin.Pointer, file.Length);
+ using var pin = file.Data.AsMemory().Pin();
+ var fdt = new FdtFileView(pin.Pointer, file.Data.Length);
return fdt.MaxTextureIndex;
}
})));
@@ -101,11 +144,13 @@ internal sealed partial class FontAtlasFactory
{
unsafe
{
- using var pin = file.Result.AsMemory().Pin();
- var fdt = new FdtFileView(pin.Pointer, file.Result.Length);
+ using var pin = file.Result.Data.AsMemory().Pin();
+ var fdt = new FdtFileView(pin.Pointer, file.Result.Data.Length);
return fdt.ToGlyphRanges();
}
});
+
+ Task.Run(this.CheckSanity);
}
///
@@ -203,12 +248,12 @@ internal sealed partial class FontAtlasFactory
///
/// The font family and size.
/// The .
- public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas]));
+ public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas]).Data);
///
public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView)
{
- var arr = ExtractResult(this.fdtFiles[gffas]);
+ var arr = ExtractResult(this.fdtFiles[gffas]).Data;
var handle = arr.AsMemory().Pin();
try
{
@@ -340,6 +385,110 @@ internal sealed partial class FontAtlasFactory
}
}
+ private async Task CheckSanity()
+ {
+ var invalidFiles = new Dictionary();
+ var texFileTasks = new Dictionary>();
+ var foundHashes = new Dictionary();
+ foreach (var (gffas, fdtTask) in this.fdtFiles)
+ {
+ var fontAttr = gffas.GetAttribute()!;
+ try
+ {
+ foundHashes[fontAttr.Path] = Crc32.Get((await fdtTask).Data);
+
+ foreach (var (task, index) in
+ (await this.texFiles[fontAttr.TexPathFormat]).Select((x, i) => (x, i)))
+ texFileTasks[fontAttr.TexPathFormat.Format(index)] = task;
+ }
+ catch (Exception e)
+ {
+ invalidFiles[fontAttr.Path] = e;
+ }
+ }
+
+ foreach (var (path, texTask) in texFileTasks)
+ {
+ try
+ {
+ var hc = default(HashCode);
+ hc.AddBytes((await texTask).Data);
+ foundHashes[path] = Crc32.Get((await texTask).Data);
+ }
+ catch (Exception e)
+ {
+ invalidFiles[path] = e;
+ }
+ }
+
+ foreach (var (path, hashCode) in foundHashes)
+ {
+ if (!KnownFontFileDataHashes.TryGetValue(path, out var expectedHashCode))
+ continue;
+ if (expectedHashCode != hashCode)
+ {
+ invalidFiles[path] = new InvalidDataException(
+ $"Expected 0x{expectedHashCode:X08}; got 0x{hashCode:X08}");
+ }
+ }
+
+ var dconf = await Service.GetAsync();
+ var nm = await Service.GetAsync();
+ var intm = (await Service.GetAsync()).Manager;
+ var ggui = await Service.GetAsync();
+ var cstate = await Service.GetAsync();
+
+ if (invalidFiles.Any())
+ {
+ Log.Warning("Found {n} font related file(s) with unexpected hash code values.", invalidFiles.Count);
+ foreach (var (path, ex) in invalidFiles)
+ Log.Warning(ex, "\t=> {path}", path);
+ Log.Verbose(JsonConvert.SerializeObject(foundHashes));
+ if (this.DefaultFontSpec is not SingleFontSpec { FontId: GameFontAndFamilyId })
+ return;
+
+ this.Framework.Update += FrameworkOnUpdate;
+
+ void FrameworkOnUpdate(IFramework framework)
+ {
+ var charaSelect = ggui.GetAddonByName("CharaSelect", 1);
+ var charaMake = ggui.GetAddonByName("CharaMake", 1);
+ var titleDcWorldMap = ggui.GetAddonByName("TitleDCWorldMap", 1);
+
+ // Show notification when TSM is visible, so that user can check whether a font looks bad
+ if (cstate.IsLoggedIn
+ || charaMake != IntPtr.Zero
+ || charaSelect != IntPtr.Zero
+ || titleDcWorldMap != IntPtr.Zero)
+ return;
+
+ this.Framework.Update -= FrameworkOnUpdate;
+
+ var n = nm.AddNotification(
+ "Non-default game fonts detected. If things do not look right, you can use a different font. Running repairs from XIVLauncher is recommended.",
+ "Modded font warning",
+ NotificationType.Warning,
+ 10000);
+ n.UseMonospaceFont = true;
+ n.Actions.Add(
+ (
+ "Use Noto Sans",
+ () =>
+ {
+ dconf.DefaultFontSpec =
+ new SingleFontSpec
+ {
+ FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium),
+ SizePx = 17,
+ };
+ dconf.QueueSave();
+ intm.RebuildFonts();
+ }));
+ n.Actions.Add(("Dismiss", () => n.Dismissed = true));
+ }
+ }
+ }
+
private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex)
{
var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]);