From 307f0fcbe834f16d879f1d58014ee6f0ec3a2771 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sat, 17 Feb 2024 01:19:42 +0900 Subject: [PATCH] Warn if font files' hashes are unexpected (#1659) --- .../Notifications/NotificationManager.cs | 68 +++++++- .../Internals/FontAtlasFactory.cs | 165 +++++++++++++++++- 2 files changed, 216 insertions(+), 17 deletions(-) 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]);