diff --git a/.gitmodules b/.gitmodules index e265f5fc0..dd184b54e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,7 +3,7 @@ url = https://github.com/goatcorp/ImGuiScene [submodule "lib/FFXIVClientStructs"] path = lib/FFXIVClientStructs - url = https://github.com/aers/FFXIVClientStructs.git + url = https://github.com/aers/FFXIVClientStructs [submodule "lib/Nomade040-nmd"] path = lib/Nomade040-nmd url = https://github.com/Nomade040/nmd.git diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index f9959a910..3e480154c 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -27,7 +27,7 @@ - + diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index f80962a9d..38bc0b886 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.1.0.5 + 9.1.0.6 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) @@ -68,9 +68,9 @@ - + - + all diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 564b10fae..2398cdb16 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.IO; using System.Threading; @@ -52,15 +51,12 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager DefaultExcelLanguage = this.Language.ToLumina(), }; - var processModule = Process.GetCurrentProcess().MainModule; - if (processModule != null) + this.GameData = new( + Path.Combine(Path.GetDirectoryName(Environment.ProcessPath)!, "sqpack"), + luminaOptions) { - this.GameData = new GameData(Path.Combine(Path.GetDirectoryName(processModule.FileName)!, "sqpack"), luminaOptions); - } - else - { - throw new Exception("Could not main module."); - } + StreamPool = new(), + }; Log.Information("Lumina is ready: {0}", this.GameData.DataPath); @@ -107,7 +103,8 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager } catch (Exception ex) { - Log.Error(ex, "Could not download data."); + Log.Error(ex, "Could not initialize Lumina"); + throw; } } @@ -161,6 +158,7 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager void IInternalDisposableService.DisposeService() { this.luminaCancellationTokenSource.Cancel(); + this.GameData.Dispose(); } private class LauncherTroubleshootingInfo diff --git a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs index 70b0f53d2..abbfc6a5b 100644 --- a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs +++ b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs @@ -47,14 +47,14 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM this.addonContextMenuOnMenuSelectedHook.Enable(); } - private unsafe delegate ushort RaptureAtkModuleOpenAddonByAgentDelegate(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId); + private delegate ushort RaptureAtkModuleOpenAddonByAgentDelegate(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId); - private unsafe delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3); + private delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3); - private unsafe delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2); + private delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2); /// - public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened; + public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened; private Dictionary> MenuItems { get; } = new(); @@ -171,7 +171,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM private void SetupGenericMenu(int headerCount, int sizeHeaderIdx, int returnHeaderIdx, int submenuHeaderIdx, IReadOnlyList items, ref int valueCount, ref AtkValue* values) { - var itemsWithIdx = items.Select((item, idx) => (item, idx)).OrderBy(i => i.item.Priority); + var itemsWithIdx = items.Select((item, idx) => (item, idx)).OrderBy(i => i.item.Priority).ToArray(); var prefixItems = itemsWithIdx.Where(i => i.item.Priority < 0).ToArray(); var suffixItems = itemsWithIdx.Where(i => i.item.Priority >= 0).ToArray(); @@ -268,10 +268,10 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM foreach (var item in items) { - if (!item.Prefix.HasValue) + if (!item.Prefix.HasValue && !item.UseDefaultPrefix) { - item.PrefixChar = 'D'; - item.PrefixColor = 539; + item.Prefix = MenuItem.DalamudDefaultPrefix; + item.PrefixColor = MenuItem.DalamudDefaultPrefixColor; Log.Warning($"Menu item \"{item.Name}\" has no prefix, defaulting to Dalamud's. Menu items outside of a submenu must have a prefix."); } } @@ -378,13 +378,13 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM { // The in game menu actually supports 32 items, but the last item can't have a visible submenu arrow. // As such, we'll only work with 31 items. - const int MaxMenuItems = 31; - if (items.Count + nativeMenuSize > MaxMenuItems) + const int maxMenuItems = 31; + if (items.Count + nativeMenuSize > maxMenuItems) { - Log.Warning($"Menu size exceeds {MaxMenuItems} items, truncating."); + Log.Warning($"Menu size exceeds {maxMenuItems} items, truncating."); var orderedItems = items.OrderBy(i => i.Priority).ToArray(); - var newItems = orderedItems[..(MaxMenuItems - nativeMenuSize - 1)]; - var submenuItems = orderedItems[(MaxMenuItems - nativeMenuSize - 1)..]; + var newItems = orderedItems[..(maxMenuItems - nativeMenuSize - 1)]; + var submenuItems = orderedItems[(maxMenuItems - nativeMenuSize - 1)..]; return newItems.Append(new MenuItem { Prefix = SeIconChar.BoxedLetterD, @@ -450,7 +450,6 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM if (callbackId < 0) { selectedIdx = -callbackId - 1; - goto original; } else { @@ -461,17 +460,17 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM { if (item.OnClicked == null) throw new InvalidOperationException("Item has no OnClicked handler"); - item.OnClicked.InvokeSafely(new( - (name, items) => + item.OnClicked.InvokeSafely(new MenuItemClickedArgs( + (name, submenuItems) => { short x, y; addon->AtkUnitBase.GetPosition(&x, &y); - this.OpenSubmenu(name ?? item.Name, items, x, y); + this.OpenSubmenu(name ?? item.Name, submenuItems, x, y); openedSubmenu = true; }, this.SelectedParentAddon, this.SelectedAgent, - this.SelectedMenuType.Value, + this.SelectedMenuType ?? default, this.SelectedEventInterfaces)); } catch (Exception e) @@ -479,14 +478,14 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM Log.Error(e, "Error while handling context menu click"); } - // Close with clicky sound + // Close with click sound if (!openedSubmenu) addon->AtkUnitBase.FireCallbackInt(-2); return false; } original: - // Eventually handled by inventorycontext here: 14022BBD0 (6.51) + // Eventually handled by inventory context here: 14022BBD0 (6.51) return this.addonContextMenuOnMenuSelectedHook.Original(addon, selectedIdx, a3); } } @@ -511,7 +510,7 @@ internal class ContextMenuPluginScoped : IInternalDisposableService, IContextMen } /// - public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened; + public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened; private Dictionary> MenuItems { get; } = new(); diff --git a/Dalamud/Game/Gui/ContextMenu/MenuArgs.cs b/Dalamud/Game/Gui/ContextMenu/MenuArgs.cs index d0d8ec0dc..cc58a839b 100644 --- a/Dalamud/Game/Gui/ContextMenu/MenuArgs.cs +++ b/Dalamud/Game/Gui/ContextMenu/MenuArgs.cs @@ -70,8 +70,18 @@ public abstract unsafe class MenuArgs /// Almost always an agent pointer. You can use this to find out what type of context menu it is. /// /// Thrown when the context menu is not a . - public IReadOnlySet EventInterfaces => - this.MenuType != ContextMenuType.Default ? - this.eventInterfaces : - throw new InvalidOperationException("Not a default context menu"); + public IReadOnlySet EventInterfaces + { + get + { + if (this.MenuType is ContextMenuType.Default) + { + return this.eventInterfaces ?? new HashSet(); + } + else + { + throw new InvalidOperationException("Not a default context menu"); + } + } + } } diff --git a/Dalamud/Game/Gui/ContextMenu/MenuItem.cs b/Dalamud/Game/Gui/ContextMenu/MenuItem.cs index fdeb64d13..ec111f19e 100644 --- a/Dalamud/Game/Gui/ContextMenu/MenuItem.cs +++ b/Dalamud/Game/Gui/ContextMenu/MenuItem.cs @@ -10,6 +10,16 @@ namespace Dalamud.Game.Gui.ContextMenu; /// public sealed record MenuItem { + /// + /// The default prefix used if no specific preset is specified. + /// + public const SeIconChar DalamudDefaultPrefix = SeIconChar.BoxedLetterD; + + /// + /// The default prefix color used if no specific preset is specified. + /// + public const ushort DalamudDefaultPrefixColor = 539; + /// /// Gets or sets the display name of the menu item. /// @@ -46,6 +56,11 @@ public sealed record MenuItem /// Gets or sets the color of the . Specifies a row id. /// public ushort PrefixColor { get; set; } + + /// + /// Gets or sets a value indicating whether the dev wishes to intentionally use the default prefix symbol and color. + /// + public bool UseDefaultPrefix { get; set; } /// /// Gets or sets the callback to be invoked when the menu item is clicked. diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index b28c9a7d9..8a7982b07 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -73,13 +73,16 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar this.configuration.QueueSave(); } + /// + public IReadOnlyList Entries => this.entries; + /// public DtrBarEntry Get(string title, SeString? text = null) { if (this.entries.Any(x => x.Title == title) || this.newEntries.Any(x => x.Title == title)) throw new ArgumentException("An entry with the same title already exists."); - var entry = new DtrBarEntry(title, null); + var entry = new DtrBarEntry(this.configuration, title, null); entry.Text = text; // Add the entry to the end of the order list, if it's not there already. @@ -196,7 +199,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar foreach (var data in this.entries) { - var isHide = this.configuration.DtrIgnore!.Any(x => x == data.Title) || !data.Shown; + var isHide = data.UserHidden || !data.Shown; if (data is { Dirty: true, Added: true, Text: not null, TextNode: not null }) { @@ -499,6 +502,9 @@ internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar private readonly DtrBar dtrBarService = Service.Get(); private readonly Dictionary pluginEntries = new(); + + /// + public IReadOnlyList Entries => this.dtrBarService.Entries; /// void IInternalDisposableService.DisposeService() @@ -510,7 +516,7 @@ internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar this.pluginEntries.Clear(); } - + /// public DtrBarEntry Get(string title, SeString? text = null) { diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index f04e1427d..ef4ce062b 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -1,37 +1,115 @@ using System; +using System.Linq; +using Dalamud.Configuration.Internal; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Gui.Dtr; +/// +/// Interface representing a read-only entry in the server info bar. +/// +public interface IReadOnlyDtrBarEntry +{ + /// + /// Gets the title of this entry. + /// + public string Title { get; } + + /// + /// Gets a value indicating whether this entry has a click action. + /// + public bool HasClickAction { get; } + + /// + /// Gets the text of this entry. + /// + public SeString Text { get; } + + /// + /// Gets a tooltip to be shown when the user mouses over the dtr entry. + /// + public SeString Tooltip { get; } + + /// + /// Gets a value indicating whether this entry should be shown. + /// + public bool Shown { get; } + + /// + /// Gets a value indicating whether or not the user has hidden this entry from view through the Dalamud settings. + /// + public bool UserHidden { get; } + + /// + /// Triggers the click action of this entry. + /// + /// True, if a click action was registered and executed. + public bool TriggerClickAction(); +} + +/// +/// Interface representing an entry in the server info bar. +/// +public interface IDtrBarEntry : IReadOnlyDtrBarEntry +{ + /// + /// Gets or sets the text of this entry. + /// + public new SeString? Text { get; set; } + + /// + /// Gets or sets a tooltip to be shown when the user mouses over the dtr entry. + /// + public new SeString? Tooltip { get; set; } + + /// + /// Gets or sets a value indicating whether this entry is visible. + /// + public new bool Shown { get; set; } + + /// + /// Gets or sets a action to be invoked when the user clicks on the dtr entry. + /// + public Action? OnClick { get; set; } + + /// + /// Remove this entry from the bar. + /// You will need to re-acquire it from DtrBar to reuse it. + /// + public void Remove(); +} + /// /// Class representing an entry in the server info bar. /// -public sealed unsafe class DtrBarEntry : IDisposable +public sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry { + private readonly DalamudConfiguration configuration; + private bool shownBacking = true; private SeString? textBacking = null; /// /// Initializes a new instance of the class. /// + /// Dalamud configuration, used to check if the entry is hidden by the user. /// The title of the bar entry. /// The corresponding text node. - internal DtrBarEntry(string title, AtkTextNode* textNode) + internal DtrBarEntry(DalamudConfiguration configuration, string title, AtkTextNode* textNode) { + this.configuration = configuration; this.Title = title; this.TextNode = textNode; } - /// - /// Gets the title of this entry. - /// + /// public string Title { get; init; } - /// - /// Gets or sets the text of this entry. - /// + /// public SeString? Text { get => this.textBacking; @@ -41,10 +119,8 @@ public sealed unsafe class DtrBarEntry : IDisposable this.Dirty = true; } } - - /// - /// Gets or sets a tooltip to be shown when the user mouses over the dtr entry. - /// + + /// public SeString? Tooltip { get; set; } /// @@ -52,9 +128,10 @@ public sealed unsafe class DtrBarEntry : IDisposable /// public Action? OnClick { get; set; } - /// - /// Gets or sets a value indicating whether this entry is visible. - /// + /// + public bool HasClickAction => this.OnClick != null; + + /// public bool Shown { get => this.shownBacking; @@ -65,6 +142,10 @@ public sealed unsafe class DtrBarEntry : IDisposable } } + /// + [Api10ToDo("Maybe make this config scoped to internalname?")] + public bool UserHidden => this.configuration.DtrIgnore?.Any(x => x == this.Title) ?? false; + /// /// Gets or sets the internal text node of this entry. /// @@ -84,6 +165,16 @@ public sealed unsafe class DtrBarEntry : IDisposable /// Gets or sets a value indicating whether this entry has just been added. /// internal bool Added { get; set; } = false; + + /// + public bool TriggerClickAction() + { + if (this.OnClick == null) + return false; + + this.OnClick.Invoke(); + return true; + } /// /// Remove this entry from the bar. diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 7dada3ecd..66b88b23d 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -23,13 +23,16 @@ using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; +using Dalamud.Logging.Internal; using Dalamud.Utility; using Dalamud.Utility.Timing; + using ImGuiNET; + using ImGuiScene; using JetBrains.Annotations; using PInvoke; -using Serilog; + using SharpDX; using SharpDX.Direct3D; using SharpDX.Direct3D11; @@ -67,6 +70,8 @@ internal class InterfaceManager : IInternalDisposableService /// public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; + private static readonly ModuleLog Log = new("INTERFACE"); + private readonly ConcurrentBag deferredDisposeTextures = new(); private readonly ConcurrentBag deferredDisposeImFontLockeds = new(); @@ -146,6 +151,13 @@ internal class InterfaceManager : IInternalDisposableService public static ImFontPtr IconFont => WhenFontsReady().IconFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault); + /// + /// Gets an included FontAwesome icon font with fixed width. + /// Accessing this static property outside of the main thread is dangerous and not supported. + /// + public static ImFontPtr IconFontFixedWidth => + WhenFontsReady().IconFontFixedWidthHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault); + /// /// Gets an included monospaced font.
/// Accessing this static property outside of the main thread is dangerous and not supported. @@ -163,6 +175,11 @@ internal class InterfaceManager : IInternalDisposableService ///
public FontHandle? IconFontHandle { get; private set; } + /// + /// Gets the icon font handle with fixed width. + /// + public FontHandle? IconFontFixedWidthHandle { get; private set; } + /// /// Gets the mono font handle. /// @@ -396,7 +413,7 @@ internal class InterfaceManager : IInternalDisposableService }); } } - + // no sampler for now because the ImGui implementation we copied doesn't allow for changing it return new DalamudTextureWrap(new D3DTextureWrap(resView, width, height)); } @@ -492,7 +509,7 @@ internal class InterfaceManager : IInternalDisposableService atlas.BuildTask.GetAwaiter().GetResult(); return im; } - + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void RenderImGui(RawDX11Scene scene) { @@ -806,6 +823,14 @@ internal class InterfaceManager : IInternalDisposableService GlyphMinAdvanceX = DefaultFontSizePx, GlyphMaxAdvanceX = DefaultFontSizePx, }))); + this.IconFontFixedWidthHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild(tk => tk.AddDalamudAssetFont( + DalamudAsset.FontAwesomeFreeSolid, + new() + { + SizePx = Service.Get().DefaultFontSpec.SizePx, + GlyphRanges = new ushort[] { 0x20, 0x20, 0x00 }, + }))); this.MonoFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild( tk => tk.AddDalamudAssetFont( @@ -822,6 +847,13 @@ internal class InterfaceManager : IInternalDisposableService tk.GetFont(this.DefaultFontHandle), tk.GetFont(this.MonoFontHandle), missingOnly: true); + + // Fill missing glyphs in IconFontFixedWidth with IconFont and fit ratio + tk.CopyGlyphsAcrossFonts( + tk.GetFont(this.IconFontHandle), + tk.GetFont(this.IconFontFixedWidthHandle), + missingOnly: true); + tk.FitRatio(tk.GetFont(this.IconFontFixedWidthHandle)); }); this.DefaultFontHandle.ImFontChanged += (_, font) => { @@ -844,7 +876,7 @@ internal class InterfaceManager : IInternalDisposableService }); }; } - + // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. _ = this.dalamudAtlas.BuildFontsAsync(); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs index 22f615e8a..a6d76f44f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs @@ -18,6 +18,7 @@ internal class FontAwesomeTestWidget : IDataWindowWidget private int selectedIconCategory; private string iconSearchInput = string.Empty; private bool iconSearchChanged = true; + private bool useFixedWidth = false; /// public string[]? CommandShortcuts { get; init; } = { "fa", "fatest", "fontawesome" }; @@ -80,6 +81,8 @@ internal class FontAwesomeTestWidget : IDataWindowWidget { this.iconSearchChanged = true; } + + ImGui.Checkbox("Use fixed width font", ref this.useFixedWidth); ImGuiHelpers.ScaledDummy(10f); for (var i = 0; i < this.icons?.Count; i++) @@ -88,7 +91,7 @@ internal class FontAwesomeTestWidget : IDataWindowWidget ImGuiHelpers.ScaledRelativeSameLine(50f); ImGui.Text($"{this.iconNames?[i]}"); ImGuiHelpers.ScaledRelativeSameLine(280f); - ImGui.PushFont(UiBuilder.IconFont); + ImGui.PushFont(this.useFixedWidth ? InterfaceManager.IconFontFixedWidth : InterfaceManager.IconFont); ImGui.Text(this.icons[i].ToIconString()); ImGui.PopFont(); ImGuiHelpers.ScaledDummy(2f); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 455c8e4d3..80f821033 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1128,34 +1128,33 @@ 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 + private record PluginInstallerAvailablePluginProxy(RemotePluginManifest? RemoteManifest, LocalPlugin? LocalPlugin); + + private IEnumerable GatherProxies() { + var proxies = new List(); + var availableManifests = this.pluginListAvailable; var installedPlugins = this.pluginListInstalled.ToList(); // Copy intended if (availableManifests.Count == 0) { ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoCompatible); - return; + return proxies; } var filteredAvailableManifests = availableManifests - .Where(rm => !this.IsManifestFiltered(rm)) - .ToList(); + .Where(rm => !this.IsManifestFiltered(rm)) + .ToList(); if (filteredAvailableManifests.Count == 0) { ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching); - return; + return proxies; } - var proxies = new List(); - // 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()) { @@ -1168,7 +1167,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (plugin != null) { installedPlugins.Remove(plugin); - proxies.Add(new PluginInstallerAvailablePluginProxy(null, plugin)); + proxies.Add(new PluginInstallerAvailablePluginProxy(availableManifest, plugin)); continue; } @@ -1187,8 +1186,14 @@ internal class PluginInstallerWindow : Window, IDisposable proxies.Add(new PluginInstallerAvailablePluginProxy(null, installedPlugin)); } + return proxies; + } +#pragma warning restore SA1201 + + private void DrawAvailablePluginList() + { var i = 0; - foreach (var proxy in proxies) + foreach (var proxy in this.GatherProxies()) { IPluginManifest applicableManifest = proxy.LocalPlugin != null ? proxy.LocalPlugin.Manifest : proxy.RemoteManifest; @@ -1199,7 +1204,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (proxy.LocalPlugin != null) { - this.DrawInstalledPlugin(proxy.LocalPlugin, i++, true); + this.DrawInstalledPlugin(proxy.LocalPlugin, i++, proxy.RemoteManifest, true); } else if (proxy.RemoteManifest != null) { @@ -1237,7 +1242,12 @@ internal class PluginInstallerWindow : Window, IDisposable if (filterTesting && !manager.HasTestingOptIn(plugin.Manifest)) continue; - this.DrawInstalledPlugin(plugin, i++); + // Find the applicable remote manifest + var remoteManifest = this.pluginListAvailable + .FirstOrDefault(rm => rm.InternalName == plugin.Manifest.InternalName && + rm.RepoUrl == plugin.Manifest.RepoUrl); + + this.DrawInstalledPlugin(plugin, i++, remoteManifest); } } @@ -1266,7 +1276,7 @@ internal class PluginInstallerWindow : Window, IDisposable var i = 0; foreach (var plugin in filteredList) { - this.DrawInstalledPlugin(plugin, i++); + this.DrawInstalledPlugin(plugin, i++, null); } } @@ -2173,17 +2183,18 @@ internal class PluginInstallerWindow : Window, IDisposable ImGuiHelpers.ScaledDummy(10); ImGui.SameLine(); - this.DrawVisitRepoUrlButton(manifest.RepoUrl, true); + if (this.DrawVisitRepoUrlButton(manifest.RepoUrl, true)) + { + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(3); + } - ImGui.SameLine(); - ImGuiHelpers.ScaledDummy(3); - ImGui.SameLine(); - if (!manifest.SourceRepo.IsThirdParty && manifest.AcceptsFeedback) { + ImGui.SameLine(); this.DrawSendFeedbackButton(manifest, false, true); } - + ImGuiHelpers.ScaledDummy(5); if (this.DrawPluginImages(null, manifest, isThirdParty, index)) @@ -2251,7 +2262,7 @@ internal class PluginInstallerWindow : Window, IDisposable } } - private void DrawInstalledPlugin(LocalPlugin plugin, int index, bool showInstalled = false) + private void DrawInstalledPlugin(LocalPlugin plugin, int index, RemotePluginManifest? remoteManifest, bool showInstalled = false) { var configuration = Service.Get(); var commandManager = Service.Get(); @@ -2376,7 +2387,9 @@ internal class PluginInstallerWindow : Window, IDisposable } ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}"); - var hasChangelog = !plugin.Manifest.Changelog.IsNullOrEmpty(); + + var applicableChangelog = plugin.IsTesting ? remoteManifest?.Changelog : remoteManifest?.TestingChangelog; + var hasChangelog = !applicableChangelog.IsNullOrWhitespace(); var didDrawChangelogInsideCollapsible = false; if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, plugin.IsThirdParty, trouble, availablePluginUpdate != default, false, false, plugin.IsOrphaned, () => this.DrawInstalledPluginContextMenu(plugin, testingOptIn), index)) @@ -2473,11 +2486,15 @@ internal class PluginInstallerWindow : Window, IDisposable if (canFeedback) { + ImGui.SameLine(); this.DrawSendFeedbackButton(plugin.Manifest, plugin.IsTesting, false); } if (availablePluginUpdate != default && !plugin.IsDev) + { + ImGui.SameLine(); this.DrawUpdateSinglePluginButton(availablePluginUpdate); + } ImGui.SameLine(); ImGui.TextColored(ImGuiColors.DalamudGrey3, $" v{plugin.EffectiveVersion}"); @@ -2494,7 +2511,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGui.TreeNode(Locs.PluginBody_CurrentChangeLog(plugin.EffectiveVersion))) { didDrawChangelogInsideCollapsible = true; - this.DrawInstalledPluginChangelog(plugin.Manifest); + this.DrawInstalledPluginChangelog(applicableChangelog); ImGui.TreePop(); } } @@ -2502,9 +2519,10 @@ internal class PluginInstallerWindow : Window, IDisposable if (availablePluginUpdate != default && !availablePluginUpdate.UpdateManifest.Changelog.IsNullOrWhitespace()) { var availablePluginUpdateVersion = availablePluginUpdate.UseTesting ? availablePluginUpdate.UpdateManifest.TestingAssemblyVersion : availablePluginUpdate.UpdateManifest.AssemblyVersion; - if (ImGui.TreeNode(Locs.PluginBody_UpdateChangeLog(availablePluginUpdateVersion))) + var availableChangelog = availablePluginUpdate.UseTesting ? availablePluginUpdate.UpdateManifest.TestingChangelog : availablePluginUpdate.UpdateManifest.Changelog; + if (!availableChangelog.IsNullOrWhitespace() && ImGui.TreeNode(Locs.PluginBody_UpdateChangeLog(availablePluginUpdateVersion))) { - this.DrawInstalledPluginChangelog(availablePluginUpdate.UpdateManifest); + this.DrawInstalledPluginChangelog(availableChangelog); ImGui.TreePop(); } } @@ -2512,13 +2530,13 @@ internal class PluginInstallerWindow : Window, IDisposable if (thisWasUpdated && hasChangelog && !didDrawChangelogInsideCollapsible) { - this.DrawInstalledPluginChangelog(plugin.Manifest); + this.DrawInstalledPluginChangelog(applicableChangelog); } ImGui.PopID(); } - private void DrawInstalledPluginChangelog(IPluginManifest manifest) + private void DrawInstalledPluginChangelog(string changelog) { ImGuiHelpers.ScaledDummy(5); @@ -2531,7 +2549,7 @@ internal class PluginInstallerWindow : Window, IDisposable { ImGui.Text("Changelog:"); ImGuiHelpers.ScaledDummy(2); - ImGuiHelpers.SafeTextWrapped(manifest.Changelog!); + ImGuiHelpers.SafeTextWrapped(changelog!); } ImGui.EndChild(); @@ -2878,8 +2896,6 @@ internal class PluginInstallerWindow : Window, IDisposable private void DrawUpdateSinglePluginButton(AvailablePluginUpdate update) { - ImGui.SameLine(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Download)) { Task.Run(() => this.UpdateSinglePlugin(update)); @@ -2949,8 +2965,6 @@ internal class PluginInstallerWindow : Window, IDisposable private void DrawSendFeedbackButton(IPluginManifest manifest, bool isTesting, bool big) { - ImGui.SameLine(); - var clicked = big ? ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Comment, Locs.FeedbackModal_Title) : ImGuiComponents.IconButton(FontAwesomeIcon.Comment); @@ -3195,7 +3209,7 @@ internal class PluginInstallerWindow : Window, IDisposable } } - private void DrawVisitRepoUrlButton(string? repoUrl, bool big) + private bool DrawVisitRepoUrlButton(string? repoUrl, bool big) { if (!string.IsNullOrEmpty(repoUrl) && repoUrl.StartsWith("https://")) { @@ -3222,7 +3236,11 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.PluginButtonToolTip_VisitPluginUrl); + + return true; } + + return false; } private bool DrawPluginImages(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, int index) diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 9c385a99c..0600ec001 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -142,6 +142,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable var scale = ImGui.GetIO().FontGlobalScale; var entries = this.titleScreenMenu.Entries; + var hovered = ImGui.IsWindowHovered( + ImGuiHoveredFlags.RootAndChildWindows | + ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); + + Service.Get().OverrideGameCursor = !hovered; + switch (this.state) { case State.Show: @@ -187,8 +193,11 @@ internal class TitleScreenMenuWindow : Window, IDisposable i++; } - if (!ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows | - ImGuiHoveredFlags.AllowWhenBlockedByActiveItem)) + // Don't check for hover if we're in the middle of an animation, as it will cause flickering. + if (this.moveEasings.Any(x => !x.Value.IsDone)) + break; + + if (!hovered) { this.state = State.FadeOut; } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs index 827187063..2df0deae6 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal; using Dalamud.Utility; using ImGuiNET; @@ -27,6 +27,13 @@ public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit /// The texture index. int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError); + /// + /// Fits a font to a fixed 1:1 ratio adjusting glyph positions horizontally and vertically to fit within font size boundaries. + /// + /// The font to fit. + /// Whether to call target.BuildLookupTable(). + void FitRatio(ImFontPtr font, bool rebuildLookupTable = true); + /// /// Copies glyphs across fonts, in a safer way.
/// If the font does not belong to the current atlas, this function is a no-op. diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 34d28ccbd..d05e5a2e7 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -1,4 +1,4 @@ -using System.Buffers; +using System.Buffers; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -166,7 +166,7 @@ internal sealed partial class FontAtlasFactory /// public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) => this.data.AddNewTexture(textureWrap, disposeOnError); - + /// public void RegisterPostBuild(Action action) => this.registeredPostBuildActions.Add(action); @@ -391,12 +391,10 @@ internal sealed partial class FontAtlasFactory }); case DalamudAsset.LodestoneGameSymbol when !this.factory.HasGameSymbolsFontFile: - { return this.AddGameGlyphs( new(GameFontFamily.Axis, fontConfig.SizePx), fontConfig.GlyphRanges, fontConfig.MergeFont); - } default: return this.factory.AddFont( @@ -858,5 +856,30 @@ internal sealed partial class FontAtlasFactory } } } + + /// + public void FitRatio(ImFontPtr font, bool rebuildLookupTable = true) + { + var nsize = font.FontSize; + var glyphs = font.GlyphsWrapped(); + foreach (ref var glyph in glyphs.DataSpan) + { + var ratio = 1f; + if (glyph.X1 - glyph.X0 > nsize) + ratio = Math.Max(ratio, (glyph.X1 - glyph.X0) / nsize); + if (glyph.Y1 - glyph.Y0 > nsize) + ratio = Math.Max(ratio, (glyph.Y1 - glyph.Y0) / nsize); + var w = MathF.Round((glyph.X1 - glyph.X0) / ratio, MidpointRounding.ToZero); + var h = MathF.Round((glyph.Y1 - glyph.Y0) / ratio, MidpointRounding.AwayFromZero); + glyph.X0 = MathF.Round((nsize - w) / 2f, MidpointRounding.ToZero); + glyph.Y0 = MathF.Round((nsize - h) / 2f, MidpointRounding.AwayFromZero); + glyph.X1 = glyph.X0 + w; + glyph.Y1 = glyph.Y0 + h; + glyph.AdvanceX = nsize; + } + + if (rebuildLookupTable) + this.BuildLookupTable(font); + } } } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index b80fe0b82..9440b89e6 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -21,9 +21,13 @@ using Dalamud.Plugin; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; + using ImGuiNET; + using ImGuiScene; + using Serilog; + using SharpDX.Direct3D11; namespace Dalamud.Interface; @@ -53,6 +57,7 @@ public sealed class UiBuilder : IDisposable private IFontHandle? defaultFontHandle; private IFontHandle? iconFontHandle; private IFontHandle? monoFontHandle; + private IFontHandle? iconFontFixedWidthHandle; /// /// Initializes a new instance of the class and registers it. @@ -106,7 +111,7 @@ public sealed class UiBuilder : IDisposable /// Event that is fired when the plugin should open its configuration interface. /// public event Action OpenConfigUi; - + /// /// Event that is fired when the plugin should open its main interface. /// @@ -251,6 +256,16 @@ public sealed class UiBuilder : IDisposable this.InterfaceManagerWithScene?.IconFontHandle ?? throw new InvalidOperationException("Scene is not yet ready."))); + /// + /// Gets the default Dalamud icon font based on FontAwesome 5 free solid with a fixed width and vertically centered glyphs. + /// + public IFontHandle IconFontFixedWidthHandle => + this.iconFontFixedWidthHandle ??= + this.scopedFinalizer.Add( + new FontHandleWrapper( + this.InterfaceManagerWithScene?.IconFontFixedWidthHandle + ?? throw new InvalidOperationException("Scene is not yet ready."))); + /// /// Gets the default Dalamud monospaced font based on Inconsolata Regular. /// @@ -266,7 +281,7 @@ public sealed class UiBuilder : IDisposable /// new() { SizePx = UiBuilder.DefaultFontSizePx }))); /// /// - public IFontHandle MonoFontHandle => + public IFontHandle MonoFontHandle => this.monoFontHandle ??= this.scopedFinalizer.Add( new FontHandleWrapper( @@ -630,7 +645,7 @@ public sealed class UiBuilder : IDisposable { this.OpenConfigUi?.InvokeSafely(); } - + /// /// Open the registered configuration UI, if it exists. /// @@ -838,5 +853,5 @@ public sealed class UiBuilder : IDisposable private void WrappedOnImFontChanged(IFontHandle obj, ILockedImFont lockedFont) => this.ImFontChanged?.Invoke(obj, lockedFont); - } + } } diff --git a/Dalamud/Plugin/InstalledPluginState.cs b/Dalamud/Plugin/InstalledPluginState.cs index 322db3423..ba9e6f879 100644 --- a/Dalamud/Plugin/InstalledPluginState.cs +++ b/Dalamud/Plugin/InstalledPluginState.cs @@ -1,5 +1,8 @@ using System; +using Dalamud.Utility; + namespace Dalamud.Plugin; +[Api10ToDo("Refactor into an interface, add wrappers for OpenMainUI and OpenConfigUI")] public record InstalledPluginState(string Name, string InternalName, bool IsLoaded, Version Version); diff --git a/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs index 952650c72..e3d99a85a 100644 --- a/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/RemotePluginManifest.cs @@ -16,6 +16,11 @@ internal record RemotePluginManifest : PluginManifest ///
[JsonIgnore] public PluginRepository SourceRepo { get; set; } = null!; + + /// + /// Gets or sets the changelog to be shown when obtaining the testing version of the plugin. + /// + public string? TestingChangelog { get; set; } /// /// Gets a value indicating whether this plugin is eligible for testing. diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs index 54adf2163..ead4d8df9 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs @@ -166,7 +166,12 @@ internal class CallGateChannel if (arg == null) { if (paramType.IsValueType) + { + if (paramType.IsGenericType && paramType.GetGenericTypeDefinition() == typeof(Nullable<>)) + continue; + throw new IpcValueNullError(this.Name, paramType, i); + } continue; } diff --git a/Dalamud/Plugin/Services/IContextMenu.cs b/Dalamud/Plugin/Services/IContextMenu.cs index 4d792116d..a3bfa722a 100644 --- a/Dalamud/Plugin/Services/IContextMenu.cs +++ b/Dalamud/Plugin/Services/IContextMenu.cs @@ -1,6 +1,4 @@ using Dalamud.Game.Gui.ContextMenu; -using Dalamud.Game.Text; -using Dalamud.Game.Text.SeStringHandling; namespace Dalamud.Plugin.Services; @@ -16,8 +14,9 @@ public interface IContextMenu public delegate void OnMenuOpenedDelegate(MenuOpenedArgs args); /// - /// Event that gets fired every time the game framework updates. + /// Event that gets fired whenever any context menu is opened. /// + /// Use this event and then check if the triggering addon is the desired addon, then add custom context menu items to the provided args. event OnMenuOpenedDelegate OnMenuOpened; /// @@ -25,6 +24,7 @@ public interface IContextMenu /// /// The type of context menu to add the item to. /// The item to add. + /// Used to add a context menu entry to all context menus. void AddMenuItem(ContextMenuType menuType, MenuItem item); /// @@ -32,6 +32,7 @@ public interface IContextMenu /// /// The type of context menu to remove the item from. /// The item to add. + /// Used to remove a context menu entry from all context menus. /// if the item was removed, if it was not found. bool RemoveMenuItem(ContextMenuType menuType, MenuItem item); } diff --git a/Dalamud/Plugin/Services/IDtrBar.cs b/Dalamud/Plugin/Services/IDtrBar.cs index a5a750cf6..6019bb1e6 100644 --- a/Dalamud/Plugin/Services/IDtrBar.cs +++ b/Dalamud/Plugin/Services/IDtrBar.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using Dalamud.Game.Gui.Dtr; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Utility; namespace Dalamud.Plugin.Services; @@ -10,6 +12,11 @@ namespace Dalamud.Plugin.Services; /// public interface IDtrBar { + /// + /// Gets a read-only list of all DTR bar entries. + /// + public IReadOnlyList Entries { get; } + /// /// Get a DTR bar entry. /// This allows you to add your own text, and users to sort it. @@ -18,6 +25,7 @@ public interface IDtrBar /// The text the entry shows. /// The entry object used to update, hide and remove the entry. /// Thrown when an entry with the specified title exists. + [Api10ToDo("Return IDtrBarEntry instead of DtrBarEntry")] public DtrBarEntry Get(string title, SeString? text = null); /// diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index bdc0918b0..75daf9187 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -43,6 +43,7 @@ internal static class ServiceManager #endif private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); + private static readonly CancellationTokenSource UnloadCancellationTokenSource = new(); private static ManualResetEvent unloadResetEvent = new(false); @@ -107,6 +108,12 @@ internal static class ServiceManager /// Gets task that gets completed when all blocking early loading services are done loading. /// public static Task BlockingResolved { get; } = BlockingServicesLoadedTaskCompletionSource.Task; + + /// + /// Gets a cancellation token that will be cancelled once Dalamud needs to unload, be it due to a failure state + /// during initialization or during regular operation. + /// + public static CancellationToken UnloadCancellationToken => UnloadCancellationTokenSource.Token; /// /// Initializes Provided Services and FFXIVClientStructs. @@ -374,6 +381,8 @@ internal static class ServiceManager } catch (Exception e) { + UnloadCancellationTokenSource.Cancel(); + Log.Error(e, "Failed resolving services"); try { @@ -401,6 +410,8 @@ internal static class ServiceManager /// public static void UnloadAllServices() { + UnloadCancellationTokenSource.Cancel(); + var framework = Service.GetNullable(Service.ExceptionPropagationMode.None); if (framework is { IsInFrameworkUpdateThread: false, IsFrameworkUnloading: false }) { diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index 165257ef5..a4d404e18 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -116,7 +116,7 @@ internal static class Service where T : IServiceType #endif if (!instanceTcs.Task.IsCompleted) - instanceTcs.Task.Wait(); + instanceTcs.Task.Wait(ServiceManager.UnloadCancellationToken); return instanceTcs.Task.Result; } diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 89146c769..f51fc5eb5 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -344,7 +344,10 @@ public static class Util _ = NativeFunctions.MessageBoxW(Process.GetCurrentProcess().MainWindowHandle, message, caption, flags); if (exit) + { + Log.CloseAndFlush(); Environment.Exit(-1); + } } /// diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 98c1de8b9..3d572e530 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 98c1de8b94bcdfce4dc79a61cc0e8b17773777f0 +Subproject commit 3d572e5301fbcb3194d59d7c995db067eba5cc0b