using System.Collections.Generic; using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; using Dalamud.Utility; namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// /// A font handle representing a user-callback generated font. /// internal sealed class DelegateFontHandle : FontHandle { /// /// Initializes a new instance of the class. /// /// An instance of . /// Callback for . public DelegateFontHandle(IFontHandleManager manager, FontAtlasBuildStepDelegate callOnBuildStepChange) : base(manager) { this.CallOnBuildStepChange = callOnBuildStepChange; } /// /// Gets the function to be called on build step changes. /// public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; } /// /// Manager for s. /// internal sealed class HandleManager : IFontHandleManager { private readonly HashSet handles = new(); private readonly object syncRoot = new(); /// /// Initializes a new instance of the class. /// /// The name of the owner atlas. public HandleManager(string atlasName) => this.Name = $"{atlasName}:{nameof(DelegateFontHandle)}:Manager"; /// public event Action? RebuildRecommend; /// public string Name { get; } /// public IFontHandleSubstance? Substance { get; set; } /// public void Dispose() { lock (this.syncRoot) this.handles.Clear(); } /// public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) { var key = new DelegateFontHandle(this, buildStepDelegate); lock (this.syncRoot) this.handles.Add(key); this.RebuildRecommend.InvokeSafely(); return key; } /// public void FreeFontHandle(IFontHandle handle) { if (handle is not DelegateFontHandle cgfh) return; lock (this.syncRoot) this.handles.Remove(cgfh); } /// public IFontHandleSubstance NewSubstance(IRefCountable dataRoot) { lock (this.syncRoot) return new HandleSubstance(this, dataRoot, this.handles.ToArray()); } } /// /// Substance from . /// 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 fonts = new(); private readonly Dictionary buildExceptions = new(); /// /// Initializes a new instance of the class. /// /// The manager. /// The data root. /// The relevant handles. 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; } /// /// Gets the relevant handles. /// // Not owned by this class. Do not dispose. public DelegateFontHandle[] RelevantHandles { get; } /// ICollection IFontHandleSubstance.RelevantHandles => this.RelevantHandles; /// public IRefCountable DataRoot { get; } /// public IFontHandleManager Manager { get; } /// public void Dispose() { this.fonts.Clear(); this.buildExceptions.Clear(); } /// public ImFontPtr GetFontPtr(IFontHandle handle) => handle is DelegateFontHandle cgfh ? this.fonts.GetValueOrDefault(cgfh) : default; /// public Exception? GetBuildException(IFontHandle handle) => handle is DelegateFontHandle cgfh ? this.buildExceptions.GetValueOrDefault(cgfh) : default; /// 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].Handle == toolkitPreBuild.Font.Handle) 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].Handle == fontsVector[j].Handle) 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.Handle) // 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()); } } } } /// public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) { // irrelevant } /// 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)); } } } } }