From 418a2567a97d6107a41772e7498cf6590ddbce0e Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 19 Jun 2023 19:07:41 +0200 Subject: [PATCH 01/28] fix: lock plugin lists when sorting --- Dalamud/Game/ChatHandlers.cs | 4 +- .../Interface/Internal/DalamudInterface.cs | 2 +- .../PluginInstaller/PluginInstallerWindow.cs | 1 + Dalamud/Plugin/Internal/PluginManager.cs | 211 ++++++++++++------ 4 files changed, 143 insertions(+), 75 deletions(-) diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 7cd0869aa..ed69b7bbe 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -292,7 +292,7 @@ public class ChatHandlers : IServiceType if (chatGui == null || pluginManager == null || notifications == null) return; - if (!pluginManager.ReposReady || pluginManager.InstalledPlugins.Count == 0 || pluginManager.AvailablePlugins.Count == 0) + if (!pluginManager.ReposReady || !pluginManager.InstalledPlugins.Any() || !pluginManager.AvailablePlugins.Any()) { // Plugins aren't ready yet. // TODO: We should retry. This sucks, because it means we won't ever get here again until another notice. @@ -311,7 +311,7 @@ public class ChatHandlers : IServiceType return; } - var updatedPlugins = task.Result; + var updatedPlugins = task.Result.ToList(); if (updatedPlugins.Any()) { if (this.configuration.AutoUpdatePlugins) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index ca7cbe287..caf38d4d7 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -900,7 +900,7 @@ internal class DalamudInterface : IDisposable, IServiceType ImGui.Separator(); ImGui.MenuItem("API Level:" + PluginManager.DalamudApiLevel, false); - ImGui.MenuItem("Loaded plugins:" + pluginManager.InstalledPlugins.Count, false); + ImGui.MenuItem("Loaded plugins:" + pluginManager.InstalledPlugins.Count(), false); ImGui.EndMenu(); } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 33b80cf93..af642d9cf 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2898,6 +2898,7 @@ internal class PluginInstallerWindow : Window, IDisposable private void OnInstalledPluginsChanged() { var pluginManager = Service.Get(); + using var pmLock = pluginManager.LockPluginLists(); lock (this.listLock) { diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 92026cf2a..907814123 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; using System.IO; using System.IO.Compression; @@ -68,6 +67,10 @@ internal partial class PluginManager : IDisposable, IServiceType private readonly object pluginListLock = new(); private readonly DirectoryInfo pluginDirectory; private readonly BannedPlugin[]? bannedPlugins; + + private readonly List installedPluginsList = new(); + private readonly List availablePluginsList = new(); + private readonly List updatablePluginsList = new(); private readonly DalamudLinkPayload openInstallerWindowPluginChangelogsLink; @@ -145,19 +148,46 @@ internal partial class PluginManager : IDisposable, IServiceType public event Action? OnAvailablePluginsChanged; /// - /// Gets a list of all loaded plugins. + /// Gets a copy of the list of all loaded plugins. /// - public ImmutableList InstalledPlugins { get; private set; } = ImmutableList.Create(); + public IEnumerable InstalledPlugins + { + get + { + lock (this.pluginListLock) + { + return this.installedPluginsList.ToList(); + } + } + } /// /// Gets a list of all available plugins. /// - public ImmutableList AvailablePlugins { get; private set; } = ImmutableList.Create(); - + public IEnumerable AvailablePlugins + { + get + { + lock (this.pluginListLock) + { + return this.availablePluginsList.ToList(); + } + } + } + /// /// Gets a list of all plugins with an available update. /// - public ImmutableList UpdatablePlugins { get; private set; } = ImmutableList.Create(); + public IEnumerable UpdatablePlugins + { + get + { + lock (this.pluginListLock) + { + return this.updatablePluginsList.ToList(); + } + } + } /// /// Gets a list of all plugin repositories. The main repo should always be first. @@ -230,6 +260,12 @@ internal partial class PluginManager : IDisposable, IServiceType return false; } + /// + /// Get a disposable that will lock plugin lists while it is not disposed. + /// + /// The aforementioned disposable. + public IDisposable LockPluginLists() => new PluginListLockScope(this.pluginListLock); + /// /// Print to chat any plugin updates and whether they were successful. /// @@ -308,7 +344,7 @@ internal partial class PluginManager : IDisposable, IServiceType public void Dispose() { var disposablePlugins = - this.InstalledPlugins.Where(plugin => plugin.State is PluginState.Loaded or PluginState.LoadError).ToArray(); + this.installedPluginsList.Where(plugin => plugin.State is PluginState.Loaded or PluginState.LoadError).ToArray(); if (disposablePlugins.Any()) { // Unload them first, just in case some of plugin codes are still running via callbacks initiated externally. @@ -581,7 +617,7 @@ internal partial class PluginManager : IDisposable, IServiceType var sigScanner = await Service.GetAsync().ConfigureAwait(false); this.PluginsReady = true; - this.NotifyInstalledPluginsChanged(); + this.NotifyinstalledPluginsListChanged(); sigScanner.Save(); }, tokenSource.Token); @@ -596,7 +632,7 @@ internal partial class PluginManager : IDisposable, IServiceType { lock (this.pluginListLock) { - return Task.WhenAll(this.InstalledPlugins + return Task.WhenAll(this.installedPluginsList .Where(x => x.IsLoaded) .ToList() .Select(x => Task.Run(async () => await x.ReloadAsync())) @@ -629,11 +665,14 @@ internal partial class PluginManager : IDisposable, IServiceType /// Whether to notify that available plugins have changed afterwards. public void RefilterPluginMasters(bool notify = true) { - this.AvailablePlugins = this.Repos - .SelectMany(repo => repo.PluginMaster) - .Where(this.IsManifestEligible) - .Where(IsManifestVisible) - .ToImmutableList(); + lock (this.pluginListLock) + { + this.availablePluginsList.Clear(); + this.availablePluginsList.AddRange(this.Repos + .SelectMany(repo => repo.PluginMaster) + .Where(this.IsManifestEligible) + .Where(IsManifestVisible)); + } if (notify) { @@ -673,7 +712,7 @@ internal partial class PluginManager : IDisposable, IServiceType // This file is already known to us lock (this.pluginListLock) { - if (this.InstalledPlugins.Any(lp => lp.DllFile.FullName == dllFile.FullName)) + if (this.installedPluginsList.Any(lp => lp.DllFile.FullName == dllFile.FullName)) continue; } @@ -699,7 +738,7 @@ internal partial class PluginManager : IDisposable, IServiceType } if (listChanged) - this.NotifyInstalledPluginsChanged(); + this.NotifyinstalledPluginsListChanged(); } /// @@ -817,7 +856,7 @@ internal partial class PluginManager : IDisposable, IServiceType var plugin = await this.LoadPluginAsync(dllFile, manifest, reason); - this.NotifyInstalledPluginsChanged(); + this.NotifyinstalledPluginsListChanged(); return plugin; } @@ -832,12 +871,12 @@ internal partial class PluginManager : IDisposable, IServiceType lock (this.pluginListLock) { - this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); + this.installedPluginsList.Remove(plugin); } PluginLocations.Remove(plugin.AssemblyName?.FullName ?? string.Empty, out _); - this.NotifyInstalledPluginsChanged(); + this.NotifyinstalledPluginsListChanged(); this.NotifyAvailablePluginsChanged(); } @@ -937,31 +976,34 @@ internal partial class PluginManager : IDisposable, IServiceType /// Perform a dry run, don't install anything. /// If this action was performed as part of an auto-update. /// Success or failure and a list of updated plugin metadata. - public async Task> UpdatePluginsAsync(bool ignoreDisabled, bool dryRun, bool autoUpdate = false) + public async Task> UpdatePluginsAsync(bool ignoreDisabled, bool dryRun, bool autoUpdate = false) { Log.Information("Starting plugin update"); - var updatedList = new List(); + var updateTasks = new List>(); // Prevent collection was modified errors - foreach (var plugin in this.UpdatablePlugins) + lock (this.pluginListLock) { - // Can't update that! - if (plugin.InstalledPlugin.IsDev) - continue; + foreach (var plugin in this.updatablePluginsList) + { + // Can't update that! + if (plugin.InstalledPlugin.IsDev) + continue; - if (!plugin.InstalledPlugin.IsWantedByAnyProfile && ignoreDisabled) - continue; + if (!plugin.InstalledPlugin.IsWantedByAnyProfile && ignoreDisabled) + continue; - if (plugin.InstalledPlugin.Manifest.ScheduledForDeletion) - continue; + if (plugin.InstalledPlugin.Manifest.ScheduledForDeletion) + continue; - var result = await this.UpdateSinglePluginAsync(plugin, false, dryRun); - if (result != null) - updatedList.Add(result); + updateTasks.Add(this.UpdateSinglePluginAsync(plugin, false, dryRun)); + } } - this.NotifyInstalledPluginsChanged(); + var updatedList = await Task.WhenAll(updateTasks); + + this.NotifyinstalledPluginsListChanged(); this.NotifyPluginsForStateChange( autoUpdate ? PluginListInvalidationKind.AutoUpdate : PluginListInvalidationKind.Update, updatedList.Select(x => x.InternalName)); @@ -978,7 +1020,7 @@ internal partial class PluginManager : IDisposable, IServiceType /// Whether to notify that installed plugins have changed afterwards. /// Whether or not to actually perform the update, or just indicate success. /// The status of the update. - public async Task UpdateSinglePluginAsync(AvailablePluginUpdate metadata, bool notify, bool dryRun) + public async Task UpdateSinglePluginAsync(AvailablePluginUpdate metadata, bool notify, bool dryRun) { var plugin = metadata.InstalledPlugin; @@ -1025,7 +1067,7 @@ internal partial class PluginManager : IDisposable, IServiceType lock (this.pluginListLock) { - this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); + this.installedPluginsList.Remove(plugin); } } catch (Exception ex) @@ -1052,7 +1094,7 @@ internal partial class PluginManager : IDisposable, IServiceType } if (notify && updateStatus.WasUpdated) - this.NotifyInstalledPluginsChanged(); + this.NotifyinstalledPluginsListChanged(); return updateStatus; } @@ -1184,7 +1226,7 @@ internal partial class PluginManager : IDisposable, IServiceType lock (this.pluginListLock) { - foreach (var plugin in this.InstalledPlugins) + foreach (var plugin in this.installedPluginsList) { if (plugin.AssemblyName != null && plugin.AssemblyName.FullName == declaringType.Assembly.GetName().FullName) @@ -1219,11 +1261,11 @@ internal partial class PluginManager : IDisposable, IServiceType var name = manifest?.Name ?? dllFile.Name; var loadPlugin = !doNotLoad; - LocalPlugin plugin; + LocalPlugin? plugin; - if (manifest != null && manifest.InternalName == null) + if (manifest != null && (manifest.InternalName == null || manifest.Name == null)) { - Log.Error("{FileName}: Your manifest has no internal name set! Can't load this.", dllFile.FullName); + Log.Error("{FileName}: Your manifest has no internal name or name set! Can't load this.", dllFile.FullName); throw new Exception("No internal name"); } @@ -1327,9 +1369,12 @@ internal partial class PluginManager : IDisposable, IServiceType } } + if (plugin == null) + throw new Exception("Plugin was null when adding to list"); + lock (this.pluginListLock) { - this.InstalledPlugins = this.InstalledPlugins.Add(plugin); + this.installedPluginsList.Add(plugin); } return plugin; @@ -1337,39 +1382,40 @@ internal partial class PluginManager : IDisposable, IServiceType private void DetectAvailablePluginUpdates() { - var updatablePlugins = new List(); - - foreach (var plugin in this.InstalledPlugins) + lock (this.pluginListLock) { - var installedVersion = plugin.IsTesting - ? plugin.Manifest.TestingAssemblyVersion - : plugin.Manifest.AssemblyVersion; - - var updates = this.AvailablePlugins - .Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName) - .Where(remoteManifest => plugin.Manifest.InstalledFromUrl == remoteManifest.SourceRepo.PluginMasterUrl || !remoteManifest.SourceRepo.IsThirdParty) - .Where(remoteManifest => remoteManifest.DalamudApiLevel == DalamudApiLevel) - .Select(remoteManifest => - { - var useTesting = this.UseTesting(remoteManifest); - var candidateVersion = useTesting - ? remoteManifest.TestingAssemblyVersion - : remoteManifest.AssemblyVersion; - var isUpdate = candidateVersion > installedVersion; - - return (isUpdate, useTesting, candidateVersion, remoteManifest); - }) - .Where(tpl => tpl.isUpdate) - .ToList(); - - if (updates.Count > 0) + this.updatablePluginsList.Clear(); + + foreach (var plugin in this.installedPluginsList) { - var update = updates.Aggregate((t1, t2) => t1.candidateVersion > t2.candidateVersion ? t1 : t2); - updatablePlugins.Add(new AvailablePluginUpdate(plugin, update.remoteManifest, update.useTesting)); + var installedVersion = plugin.IsTesting + ? plugin.Manifest.TestingAssemblyVersion + : plugin.Manifest.AssemblyVersion; + + var updates = this.AvailablePlugins + .Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName) + .Where(remoteManifest => plugin.Manifest.InstalledFromUrl == remoteManifest.SourceRepo.PluginMasterUrl || !remoteManifest.SourceRepo.IsThirdParty) + .Where(remoteManifest => remoteManifest.DalamudApiLevel == DalamudApiLevel) + .Select(remoteManifest => + { + var useTesting = this.UseTesting(remoteManifest); + var candidateVersion = useTesting + ? remoteManifest.TestingAssemblyVersion + : remoteManifest.AssemblyVersion; + var isUpdate = candidateVersion > installedVersion; + + return (isUpdate, useTesting, candidateVersion, remoteManifest); + }) + .Where(tpl => tpl.isUpdate) + .ToList(); + + if (updates.Count > 0) + { + var update = updates.Aggregate((t1, t2) => t1.candidateVersion > t2.candidateVersion ? t1 : t2); + this.updatablePluginsList.Add(new AvailablePluginUpdate(plugin, update.remoteManifest, update.useTesting)); + } } } - - this.UpdatablePlugins = updatablePlugins.ToImmutableList(); } private void NotifyAvailablePluginsChanged() @@ -1379,7 +1425,7 @@ internal partial class PluginManager : IDisposable, IServiceType this.OnAvailablePluginsChanged?.InvokeSafely(); } - private void NotifyInstalledPluginsChanged() + private void NotifyinstalledPluginsListChanged() { this.DetectAvailablePluginUpdates(); @@ -1388,7 +1434,7 @@ internal partial class PluginManager : IDisposable, IServiceType private void NotifyPluginsForStateChange(PluginListInvalidationKind kind, IEnumerable affectedInternalNames) { - foreach (var installedPlugin in this.InstalledPlugins) + foreach (var installedPlugin in this.installedPluginsList) { if (!installedPlugin.IsLoaded || installedPlugin.DalamudInterface == null) continue; @@ -1514,4 +1560,25 @@ internal partial class PluginManager var codebasePatch = typeof(PluginManager).GetMethod(nameof(AssemblyCodeBasePatch), BindingFlags.NonPublic | BindingFlags.Static); this.assemblyCodeBaseMonoHook = new MonoMod.RuntimeDetour.Hook(codebaseTarget, codebasePatch); } + + /// + /// Scope for plugin list locks. + /// + private class PluginListLockScope : IDisposable + { + private readonly object lockObj; + + public PluginListLockScope(object lockObj) + { + this.lockObj = lockObj; + Monitor.Enter(lockObj); + } + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + Monitor.Exit(this.lockObj); + } + } } From b901ad5aff9cf75c869403d49937312f1919f64a Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 19 Jun 2023 19:19:04 +0200 Subject: [PATCH 02/28] fix: move log retention out of EntryPoint.cs, redo retention behaviour for release --- Dalamud/EntryPoint.cs | 124 ++---------------- .../Retention/DebugRetentionBehaviour.cs | 15 +++ .../Retention/ReleaseRetentionBehaviour.cs | 15 +++ .../Logging/Retention/RetentionBehaviour.cs | 91 +++++++++++++ 4 files changed, 132 insertions(+), 113 deletions(-) create mode 100644 Dalamud/Logging/Retention/DebugRetentionBehaviour.cs create mode 100644 Dalamud/Logging/Retention/ReleaseRetentionBehaviour.cs create mode 100644 Dalamud/Logging/Retention/RetentionBehaviour.cs diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 33e09e221..26ff396d1 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Logging.Internal; +using Dalamud.Logging.Retention; using Dalamud.Plugin.Internal; using Dalamud.Support; using Dalamud.Utility; @@ -88,58 +89,35 @@ public sealed class EntryPoint var logFileName = logName.IsNullOrEmpty() ? "dalamud" : $"dalamud-{logName}"; #if DEBUG - var logPath = Path.Combine(baseDirectory, $"{logFileName}.log"); - var oldPath = Path.Combine(baseDirectory, $"{logFileName}.old.log"); - var oldPathOld = Path.Combine(baseDirectory, $"{logFileName}.log.old"); + var logPath = new FileInfo(Path.Combine(baseDirectory, $"{logFileName}.log")); + var oldPath = new FileInfo(Path.Combine(baseDirectory, $"{logFileName}.old.log")); #else var logPath = Path.Combine(baseDirectory, "..", "..", "..", $"{logFileName}.log"); var oldPath = Path.Combine(baseDirectory, "..", "..", "..", $"{logFileName}.old.log"); - var oldPathOld = Path.Combine(baseDirectory, "..", "..", "..", $"{logFileName}.log.old"); #endif Log.CloseAndFlush(); - -#if DEBUG - var oldFileOld = new FileInfo(oldPathOld); - if (oldFileOld.Exists) - { - var oldFile = new FileInfo(oldPath); - if (oldFile.Exists) - oldFileOld.Delete(); - else - oldFileOld.MoveTo(oldPath); - } - CullLogFile(logPath, 1 * 1024 * 1024, oldPath, 10 * 1024 * 1024); + RetentionBehaviour behaviour; +#if DEBUG + behaviour = new DebugRetentionBehaviour(); #else - try - { - if (File.Exists(logPath)) - File.Delete(logPath); - - if (File.Exists(oldPath)) - File.Delete(oldPath); - - if (File.Exists(oldPathOld)) - File.Delete(oldPathOld); - } - catch - { - // ignored - } + behaviour = new ReleaseRetentionBehaviour(); #endif + behaviour.Apply(logPath, oldPath); + var config = new LoggerConfiguration() .WriteTo.Sink(SerilogEventSink.Instance) .MinimumLevel.ControlledBy(LogLevelSwitch); if (logSynchronously) { - config = config.WriteTo.File(logPath, fileSizeLimitBytes: null); + config = config.WriteTo.File(logPath.FullName, fileSizeLimitBytes: null); } else { config = config.WriteTo.Async(a => a.File( - logPath, + logPath.FullName, fileSizeLimitBytes: null, buffered: false, flushToDiskInterval: TimeSpan.FromSeconds(1))); @@ -265,86 +243,6 @@ public sealed class EntryPoint } } - /// - /// Trim existing log file to a specified length, and optionally move the excess data to another file. - /// - /// Target log file to trim. - /// Maximum size of target log file. - /// .old file to move excess data to. - /// Maximum size of .old file. - private static void CullLogFile(string logPath, int logMaxSize, string oldPath, int oldMaxSize) - { - var logFile = new FileInfo(logPath); - var oldFile = new FileInfo(oldPath); - var targetFiles = new[] - { - (logFile, logMaxSize), - (oldFile, oldMaxSize), - }; - var buffer = new byte[4096]; - - try - { - if (!logFile.Exists) - logFile.Create().Close(); - - // 1. Move excess data from logFile to oldFile - if (logFile.Length > logMaxSize) - { - using var reader = logFile.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - using var writer = oldFile.Open(FileMode.Append, FileAccess.Write, FileShare.ReadWrite); - - var amountToMove = (int)Math.Min(logFile.Length - logMaxSize, oldMaxSize); - reader.Seek(-(logMaxSize + amountToMove), SeekOrigin.End); - - for (var i = 0; i < amountToMove; i += buffer.Length) - writer.Write(buffer, 0, reader.Read(buffer, 0, Math.Min(buffer.Length, amountToMove - i))); - } - - // 2. Cull each of .log and .old files - foreach (var (file, maxSize) in targetFiles) - { - if (!file.Exists || file.Length <= maxSize) - continue; - - using var reader = file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); - using var writer = file.Open(FileMode.Open, FileAccess.Write, FileShare.ReadWrite); - - reader.Seek(file.Length - maxSize, SeekOrigin.Begin); - for (int read; (read = reader.Read(buffer, 0, buffer.Length)) > 0;) - writer.Write(buffer, 0, read); - - writer.SetLength(maxSize); - } - } - catch (Exception ex) - { - if (ex is IOException) - { - foreach (var (file, _) in targetFiles) - { - try - { - if (file.Exists) - file.Delete(); - } - catch (Exception ex2) - { - Log.Error(ex2, "Failed to delete {file}", file.FullName); - } - } - } - - Log.Error(ex, "Log cull failed"); - - /* - var caption = "XIVLauncher Error"; - var message = $"Log cull threw an exception: {ex.Message}\n{ex.StackTrace ?? string.Empty}"; - _ = MessageBoxW(IntPtr.Zero, message, caption, MessageBoxType.IconError | MessageBoxType.Ok); - */ - } - } - private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs args) { switch (args.ExceptionObject) diff --git a/Dalamud/Logging/Retention/DebugRetentionBehaviour.cs b/Dalamud/Logging/Retention/DebugRetentionBehaviour.cs new file mode 100644 index 000000000..719ff8ece --- /dev/null +++ b/Dalamud/Logging/Retention/DebugRetentionBehaviour.cs @@ -0,0 +1,15 @@ +using System.IO; + +namespace Dalamud.Logging.Retention; + +/// +/// Class implementing log retention behaviour for debug builds. +/// +internal class DebugRetentionBehaviour : RetentionBehaviour +{ + /// + public override void Apply(FileInfo logFile, FileInfo rolloverFile) + { + CullLogFile(logFile, 1 * 1024 * 1024, rolloverFile, 10 * 1024 * 1024); + } +} diff --git a/Dalamud/Logging/Retention/ReleaseRetentionBehaviour.cs b/Dalamud/Logging/Retention/ReleaseRetentionBehaviour.cs new file mode 100644 index 000000000..79f272c46 --- /dev/null +++ b/Dalamud/Logging/Retention/ReleaseRetentionBehaviour.cs @@ -0,0 +1,15 @@ +using System.IO; + +namespace Dalamud.Logging.Retention; + +/// +/// Class implementing log retention behaviour for release builds. +/// +internal class ReleaseRetentionBehaviour : RetentionBehaviour +{ + /// + public override void Apply(FileInfo logFile, FileInfo rolloverFile) + { + CullLogFile(logFile, 0, rolloverFile, 10 * 1024 * 1024); + } +} diff --git a/Dalamud/Logging/Retention/RetentionBehaviour.cs b/Dalamud/Logging/Retention/RetentionBehaviour.cs new file mode 100644 index 000000000..66c4c5f97 --- /dev/null +++ b/Dalamud/Logging/Retention/RetentionBehaviour.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; + +using Serilog; + +namespace Dalamud.Logging.Retention; + +/// +/// Class implementing retention behaviour for log files. +/// +internal abstract class RetentionBehaviour +{ + /// + /// Apply the specified retention behaviour to log files. + /// + /// The regular log file path. + /// The rollover "old" log file path. + public abstract void Apply(FileInfo logFile, FileInfo rolloverFile); + + /// + /// Trim existing log file to a specified length, and optionally move the excess data to another file. + /// + /// Target log file to trim. + /// Maximum size of target log file. + /// .old file to move excess data to. + /// Maximum size of .old file. + protected static void CullLogFile(FileInfo logFile, int logMaxSize, FileInfo oldFile, int oldMaxSize) + { + var targetFiles = new[] + { + (logFile, logMaxSize), + (oldFile, oldMaxSize), + }; + var buffer = new byte[4096]; + + try + { + if (!logFile.Exists) + logFile.Create().Close(); + + // 1. Move excess data from logFile to oldFile + if (logFile.Length > logMaxSize) + { + using var reader = logFile.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var writer = oldFile.Open(FileMode.Append, FileAccess.Write, FileShare.ReadWrite); + + var amountToMove = (int)Math.Min(logFile.Length - logMaxSize, oldMaxSize); + reader.Seek(-(logMaxSize + amountToMove), SeekOrigin.End); + + for (var i = 0; i < amountToMove; i += buffer.Length) + writer.Write(buffer, 0, reader.Read(buffer, 0, Math.Min(buffer.Length, amountToMove - i))); + } + + // 2. Cull each of .log and .old files + foreach (var (file, maxSize) in targetFiles) + { + if (!file.Exists || file.Length <= maxSize) + continue; + + using var reader = file.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + using var writer = file.Open(FileMode.Open, FileAccess.Write, FileShare.ReadWrite); + + reader.Seek(file.Length - maxSize, SeekOrigin.Begin); + for (int read; (read = reader.Read(buffer, 0, buffer.Length)) > 0;) + writer.Write(buffer, 0, read); + + writer.SetLength(maxSize); + } + } + catch (Exception ex) + { + if (ex is IOException) + { + foreach (var (file, _) in targetFiles) + { + try + { + if (file.Exists) + file.Delete(); + } + catch (Exception ex2) + { + Log.Error(ex2, "Failed to delete {file}", file.FullName); + } + } + } + + Log.Error(ex, "Log cull failed"); + } + } +} From 2e380b10d5b5099a9371a9db9ac0919c5d13ae7e Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 19 Jun 2023 19:52:31 +0200 Subject: [PATCH 03/28] feat: allow specifying logging path via --logpath, make sure serilog can always write to injector log --- Dalamud.Injector/EntryPoint.cs | 39 ++++++++++++++----- Dalamud/DalamudStartInfo.cs | 6 +++ Dalamud/EntryPoint.cs | 10 ++--- .../Interface/Internal/DalamudInterface.cs | 2 +- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index c4c553a47..ea0d1fadc 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -115,9 +115,9 @@ namespace Dalamud.Injector } } - private static string GetLogPath(string fileName, string logName) + private static string GetLogPath(string? baseDirectory, string fileName, string? logName) { - var baseDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + baseDirectory ??= Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); fileName = !string.IsNullOrEmpty(logName) ? $"{fileName}-{logName}.log" : $"{fileName}.log"; #if DEBUG @@ -168,6 +168,7 @@ namespace Dalamud.Injector Log.Error("A fatal error has occurred: {Exception}", eventArgs.ExceptionObject.ToString()); } + Log.CloseAndFlush(); Environment.Exit(-1); }; } @@ -180,15 +181,18 @@ namespace Dalamud.Injector }; var logName = args.FirstOrDefault(x => x.StartsWith("--logname="))?[10..]; - var logPath = GetLogPath("dalamud.injector", logName); + var logBaseDir = args.FirstOrDefault(x => x.StartsWith("--logpath="))?[10..]; + var logPath = GetLogPath(logBaseDir, "dalamud.injector", logName); CullLogFile(logPath, 1 * 1024 * 1024); Log.Logger = new LoggerConfiguration() - .WriteTo.Console(standardErrorFromLevel: LogEventLevel.Verbose) - .WriteTo.Async(a => a.File(logPath)) - .MinimumLevel.ControlledBy(levelSwitch) - .CreateLogger(); + .WriteTo.File(logPath, fileSizeLimitBytes: null) + .MinimumLevel.ControlledBy(levelSwitch) + .CreateLogger(); + + Log.Information(new string('-', 80)); + Log.Information("Dalamud.Injector, (c) 2023 XIVLauncher Contributors"); } private static void CullLogFile(string logPath, int cullingFileSize) @@ -199,9 +203,10 @@ namespace Dalamud.Injector var logFile = new FileInfo(logPath); + // Leave it to serilog if (!logFile.Exists) { - logFile.Create(); + return; } if (logFile.Length <= cullingFileSize) @@ -256,6 +261,7 @@ namespace Dalamud.Injector var assetDirectory = startInfo.AssetDirectory; var delayInitializeMs = startInfo.DelayInitializeMs; var logName = startInfo.LogName; + var logPath = startInfo.LogPath; var languageStr = startInfo.Language.ToString().ToLowerInvariant(); var troubleshootingData = "{\"empty\": true, \"description\": \"No troubleshooting data supplied.\"}"; @@ -293,6 +299,10 @@ namespace Dalamud.Injector { logName = args[i][key.Length..]; } + else if (args[i].StartsWith(key = "--logpath=")) + { + logPath = args[i][key.Length..]; + } else { continue; @@ -357,11 +367,19 @@ namespace Dalamud.Injector startInfo.GameVersion = null; startInfo.TroubleshootingPackData = troubleshootingData; startInfo.LogName = logName; + startInfo.LogPath = logPath; + + // TODO: XL should set --logpath to its roaming path. We are only doing this here until that's rolled out. +#if DEBUG + startInfo.LogPath ??= startInfo.WorkingDirectory; +#else + startInfo.LogPath = xivlauncherDir; +#endif // Set boot defaults startInfo.BootShowConsole = args.Contains("--console"); startInfo.BootEnableEtw = args.Contains("--etw"); - startInfo.BootLogPath = GetLogPath("dalamud.boot", startInfo.LogName); + startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName); startInfo.BootEnabledGameFixes = new List { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess", "backup_userdata_save", "clr_failfast_hijack", "prevent_icmphandle_crashes" }; startInfo.BootDotnetOpenProcessHookMode = 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0; @@ -418,6 +436,7 @@ namespace Dalamud.Injector Console.WriteLine("Enable VEH:\t[--veh], [--veh-full]"); Console.WriteLine("Show messagebox:\t[--msgbox1], [--msgbox2], [--msgbox3]"); Console.WriteLine("No plugins:\t[--no-plugin] [--no-3rd-plugin]"); + Console.WriteLine("Logging:\t[--logname=] [--logpath=]"); return 0; } @@ -520,6 +539,7 @@ namespace Dalamud.Injector foreach (var process in processes) Inject(process, AdjustStartInfo(dalamudStartInfo, process.MainModule.FileName), tryFixAcl); + Log.CloseAndFlush(); return 0; } @@ -808,6 +828,7 @@ namespace Dalamud.Injector Console.WriteLine($"{{\"pid\": {process.Id}, \"handle\": {processHandleForOwner}}}"); + Log.CloseAndFlush(); return 0; } diff --git a/Dalamud/DalamudStartInfo.cs b/Dalamud/DalamudStartInfo.cs index 4c8e7566d..63a61c97e 100644 --- a/Dalamud/DalamudStartInfo.cs +++ b/Dalamud/DalamudStartInfo.cs @@ -28,6 +28,7 @@ public record DalamudStartInfo : IServiceType { this.WorkingDirectory = other.WorkingDirectory; this.ConfigurationPath = other.ConfigurationPath; + this.LogPath = other.LogPath; this.LogName = other.LogName; this.PluginDirectory = other.PluginDirectory; this.AssetDirectory = other.AssetDirectory; @@ -61,6 +62,11 @@ public record DalamudStartInfo : IServiceType /// public string? ConfigurationPath { get; set; } + /// + /// Gets or sets the path of the log files. + /// + public string? LogPath { get; set; } + /// /// Gets or sets the name of the log file. /// diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 26ff396d1..19b4f841c 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -88,13 +88,9 @@ public sealed class EntryPoint { var logFileName = logName.IsNullOrEmpty() ? "dalamud" : $"dalamud-{logName}"; -#if DEBUG var logPath = new FileInfo(Path.Combine(baseDirectory, $"{logFileName}.log")); var oldPath = new FileInfo(Path.Combine(baseDirectory, $"{logFileName}.old.log")); -#else - var logPath = Path.Combine(baseDirectory, "..", "..", "..", $"{logFileName}.log"); - var oldPath = Path.Combine(baseDirectory, "..", "..", "..", $"{logFileName}.old.log"); -#endif + Log.CloseAndFlush(); RetentionBehaviour behaviour; @@ -137,7 +133,7 @@ public sealed class EntryPoint private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEvent) { // Setup logger - InitLogging(info.WorkingDirectory!, info.BootShowConsole, true, info.LogName); + InitLogging(info.LogPath!, info.BootShowConsole, true, info.LogName); SerilogEventSink.Instance.LogLine += SerilogOnLogLine; // Load configuration first to get some early persistent state, like log level @@ -145,7 +141,7 @@ public sealed class EntryPoint // Set the appropriate logging level from the configuration if (!configuration.LogSynchronously) - InitLogging(info.WorkingDirectory!, info.BootShowConsole, configuration.LogSynchronously, info.LogName); + InitLogging(info.LogPath!, info.BootShowConsole, configuration.LogSynchronously, info.LogName); LogLevelSwitch.MinimumLevel = configuration.LogLevel; // Log any unhandled exception. diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index caf38d4d7..202b394c6 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -642,7 +642,7 @@ internal class DalamudInterface : IDisposable, IServiceType configuration.QueueSave(); EntryPoint.InitLogging( - startInfo.WorkingDirectory!, + startInfo.LogPath!, startInfo.BootShowConsole, configuration.LogSynchronously, startInfo.LogName); From 10276a651d3806f51e6c1abecc88abb3f449c08f Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 19 Jun 2023 19:57:15 +0200 Subject: [PATCH 04/28] fix: don't always override logPath in release mode --- Dalamud.Injector/EntryPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index ea0d1fadc..244b8e3a7 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -373,7 +373,7 @@ namespace Dalamud.Injector #if DEBUG startInfo.LogPath ??= startInfo.WorkingDirectory; #else - startInfo.LogPath = xivlauncherDir; + startInfo.LogPath ??= xivlauncherDir; #endif // Set boot defaults From 3d47e05ab026c9fff87a37875fc24d444c253aa9 Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 19 Jun 2023 20:04:59 +0200 Subject: [PATCH 05/28] chore: lock plugin lists when applying profile want state --- Dalamud/Plugin/Internal/PluginManager.cs | 4 ++-- Dalamud/Plugin/Internal/Profiles/ProfileManager.cs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 907814123..7e16868ad 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -162,7 +162,7 @@ internal partial class PluginManager : IDisposable, IServiceType } /// - /// Gets a list of all available plugins. + /// Gets a copy of the list of all available plugins. /// public IEnumerable AvailablePlugins { @@ -176,7 +176,7 @@ internal partial class PluginManager : IDisposable, IServiceType } /// - /// Gets a list of all plugins with an available update. + /// Gets a copy of the list of all plugins with an available update. /// public IEnumerable UpdatablePlugins { diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index d91db1283..c996a3f43 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -169,6 +169,9 @@ internal class ProfileManager : IServiceType /// public void ApplyAllWantStates() { + var pm = Service.Get(); + using var pmLock = pm.LockPluginLists(); + this.isBusy = true; Log.Information("Getting want states..."); @@ -185,7 +188,6 @@ internal class ProfileManager : IServiceType Log.Information("Applying want states..."); - var pm = Service.Get(); var tasks = new List(); foreach (var installedPlugin in pm.InstalledPlugins) From 28e9d5f156287fda34a919b5ebd59b5ece75f976 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 20 Jun 2023 19:20:08 +0200 Subject: [PATCH 06/28] fix: lock profile manager while changing state --- .../PluginInstaller/PluginInstallerWindow.cs | 2 +- Dalamud/Plugin/Internal/PluginManager.cs | 23 +------ Dalamud/Plugin/Internal/Profiles/Profile.cs | 6 ++ .../Internal/Profiles/ProfileManager.cs | 60 ++++++++++++------- Dalamud/Utility/ScopedSyncRoot.cs | 29 +++++++++ 5 files changed, 77 insertions(+), 43 deletions(-) create mode 100644 Dalamud/Utility/ScopedSyncRoot.cs diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index af642d9cf..7679212e6 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2898,7 +2898,7 @@ internal class PluginInstallerWindow : Window, IDisposable private void OnInstalledPluginsChanged() { var pluginManager = Service.Get(); - using var pmLock = pluginManager.LockPluginLists(); + using var pmLock = pluginManager.GetSyncScope(); lock (this.listLock) { diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 7e16868ad..2f6a0606e 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -264,7 +264,7 @@ internal partial class PluginManager : IDisposable, IServiceType /// Get a disposable that will lock plugin lists while it is not disposed. /// /// The aforementioned disposable. - public IDisposable LockPluginLists() => new PluginListLockScope(this.pluginListLock); + public IDisposable GetSyncScope() => new ScopedSyncRoot(this.pluginListLock); /// /// Print to chat any plugin updates and whether they were successful. @@ -1560,25 +1560,4 @@ internal partial class PluginManager var codebasePatch = typeof(PluginManager).GetMethod(nameof(AssemblyCodeBasePatch), BindingFlags.NonPublic | BindingFlags.Static); this.assemblyCodeBaseMonoHook = new MonoMod.RuntimeDetour.Hook(codebaseTarget, codebasePatch); } - - /// - /// Scope for plugin list locks. - /// - private class PluginListLockScope : IDisposable - { - private readonly object lockObj; - - public PluginListLockScope(object lockObj) - { - this.lockObj = lockObj; - Monitor.Enter(lockObj); - } - - /// - public void Dispose() - { - GC.SuppressFinalize(this); - Monitor.Exit(this.lockObj); - } - } } diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index bae3e6f95..4921db42b 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -137,6 +137,8 @@ internal class Profile /// 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. public bool? WantsPlugin(string internalName) { + using var lockScope = this.manager.GetSyncScope(); + var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); return entry?.IsEnabled; } @@ -150,6 +152,8 @@ internal class Profile /// Whether or not the current state should immediately be applied. public void AddOrUpdate(string internalName, bool state, bool apply = true) { + using var lockScope = this.manager.GetSyncScope(); + Debug.Assert(!internalName.IsNullOrEmpty(), "!internalName.IsNullOrEmpty()"); var existing = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); @@ -186,6 +190,8 @@ internal class Profile /// Whether or not the current state should immediately be applied. public void Remove(string internalName, bool apply = true) { + using var lockScope = this.manager.GetSyncScope(); + var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); if (entry == null) throw new ArgumentException($"No plugin \"{internalName}\" in profile \"{this.Guid}\""); diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index c996a3f43..6250805b0 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -41,7 +41,14 @@ internal class ProfileManager : IServiceType /// /// Gets the default profile. /// - public Profile DefaultProfile => this.profiles.First(x => x.IsDefaultProfile); + public Profile DefaultProfile + { + get + { + lock (this.profiles) + return this.profiles.First(x => x.IsDefaultProfile); + } + } /// /// Gets all profiles, including the default profile. @@ -53,6 +60,12 @@ internal class ProfileManager : IServiceType /// public bool IsBusy => this.isBusy; + /// + /// Get a disposable that will lock the profile list while it is not disposed. + /// + /// The aforementioned disposable. + public ScopedSyncRoot GetSyncScope() => new ScopedSyncRoot(this.profiles); + /// /// Check if any enabled profile wants a specific plugin enabled. /// @@ -62,27 +75,30 @@ internal class ProfileManager : IServiceType /// Whether or not the plugin shall be enabled. public bool GetWantState(string internalName, bool defaultState, bool addIfNotDeclared = true) { - var want = false; - var wasInAnyProfile = false; - - foreach (var profile in this.profiles) + lock (this.profiles) { - var state = profile.WantsPlugin(internalName); - if (state.HasValue) + var want = false; + var wasInAnyProfile = false; + + foreach (var profile in this.profiles) { - want = want || (profile.IsEnabled && state.Value); - wasInAnyProfile = true; + var state = profile.WantsPlugin(internalName); + if (state.HasValue) + { + want = want || (profile.IsEnabled && state.Value); + wasInAnyProfile = true; + } } - } - if (!wasInAnyProfile && addIfNotDeclared) - { - Log.Warning("{Name} was not in any profile, adding to default with {Default}", internalName, defaultState); - this.DefaultProfile.AddOrUpdate(internalName, defaultState, false); - return defaultState; - } + if (!wasInAnyProfile && addIfNotDeclared) + { + Log.Warning("{Name} was not in any profile, adding to default with {Default}", internalName, defaultState); + this.DefaultProfile.AddOrUpdate(internalName, defaultState, false); + return defaultState; + } - return want; + return want; + } } /// @@ -91,7 +107,10 @@ internal class ProfileManager : IServiceType /// The internal name of the plugin. /// Whether or not the plugin is in any profile. public bool IsInAnyProfile(string internalName) - => this.profiles.Any(x => x.WantsPlugin(internalName) != null); + { + lock (this.profiles) + return this.profiles.Any(x => x.WantsPlugin(internalName) != null); + } /// /// Check whether a plugin is only in the default profile. @@ -170,8 +189,9 @@ internal class ProfileManager : IServiceType public void ApplyAllWantStates() { var pm = Service.Get(); - using var pmLock = pm.LockPluginLists(); - + using var managerLock = pm.GetSyncScope(); + using var profilesLock = this.GetSyncScope(); + this.isBusy = true; Log.Information("Getting want states..."); diff --git a/Dalamud/Utility/ScopedSyncRoot.cs b/Dalamud/Utility/ScopedSyncRoot.cs new file mode 100644 index 000000000..724581f3f --- /dev/null +++ b/Dalamud/Utility/ScopedSyncRoot.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading; + +namespace Dalamud.Utility; + +/// +/// Scope for plugin list locks. +/// +public class ScopedSyncRoot : IDisposable +{ + private readonly object lockObj; + + /// + /// Initializes a new instance of the class. + /// + /// The object to lock. + public ScopedSyncRoot(object lockObj) + { + this.lockObj = lockObj; + Monitor.Enter(lockObj); + } + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + Monitor.Exit(this.lockObj); + } +} From d1c22f7dd6aa80c4072e58a760583a97140a9d83 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 20 Jun 2023 19:35:59 +0200 Subject: [PATCH 07/28] fix: guard against CommandInfo, Manifest ever becoming null --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 5 ++++- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 2 +- Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 7679212e6..be27768be 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2174,7 +2174,10 @@ internal class PluginInstallerWindow : Window, IDisposable if (plugin.IsLoaded) { var commands = commandManager.Commands - .Where(cInfo => cInfo.Value.ShowInHelp && cInfo.Value.LoaderAssemblyName == plugin.Manifest.InternalName) + .Where(cInfo => + cInfo.Value != null && + cInfo.Value.ShowInHelp && + cInfo.Value.LoaderAssemblyName == plugin.Manifest.InternalName) .ToArray(); if (commands.Any()) diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 062fc94f1..1af2165da 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -630,7 +630,7 @@ internal class LocalPlugin : IDisposable if (manifest.Exists) { // var isDisabled = this.IsDisabled; // saving the internal state because it could have been deleted - this.Manifest = LocalPluginManifest.Load(manifest); + this.Manifest = LocalPluginManifest.Load(manifest) ?? throw new Exception("Could not reload manifest."); // this.Manifest.Disabled = isDisabled; this.SaveManifest(); diff --git a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs index 8a21328c5..e142f9cb0 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPluginManifest.cs @@ -76,7 +76,7 @@ internal record LocalPluginManifest : PluginManifest /// /// Path to the manifest. /// A object. - public static LocalPluginManifest Load(FileInfo manifestFile) => JsonConvert.DeserializeObject(File.ReadAllText(manifestFile.FullName))!; + public static LocalPluginManifest? Load(FileInfo manifestFile) => JsonConvert.DeserializeObject(File.ReadAllText(manifestFile.FullName)); /// /// A standardized way to get the plugin DLL name that should accompany a manifest file. May not exist. From b4b84ad39d0462ba764f9426cac90b5aa60740b1 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 20 Jun 2023 19:59:56 +0200 Subject: [PATCH 08/28] fix: don't always reload PluginMasters when saving settings Fixes a race condition when filtering categories while two refilters were active --- .../Internal/Windows/Settings/SettingsWindow.cs | 1 - Dalamud/Plugin/Internal/PluginManager.cs | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 6dbf902eb..97d9eac5c 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -226,7 +226,6 @@ internal class SettingsWindow : Window configuration.QueueSave(); - _ = Service.Get().ReloadPluginMastersAsync(); Service.Get().RebuildFonts(); } } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 2f6a0606e..8209a457a 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -672,11 +672,11 @@ internal partial class PluginManager : IDisposable, IServiceType .SelectMany(repo => repo.PluginMaster) .Where(this.IsManifestEligible) .Where(IsManifestVisible)); - } - - if (notify) - { - this.NotifyAvailablePluginsChanged(); + + if (notify) + { + this.NotifyAvailablePluginsChanged(); + } } } From ecbb5325d7c84a7144618d13389670d22aa250e5 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 20 Jun 2023 20:11:39 +0200 Subject: [PATCH 09/28] Update ClientStructs (#1254) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 130a4df52..39b39c12a 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 130a4df5298e9bcdf343ce1d93ee0afdafac587f +Subproject commit 39b39c12a62f17925a38863ccdc54fb43809f915 From 2e1ace6afd693332eb51e0f2398788de3d29b5bc Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 20 Jun 2023 20:17:58 +0200 Subject: [PATCH 10/28] fix: untangle devplugin StartOnBoot behaviour --- Dalamud/Plugin/Internal/PluginManager.cs | 25 ++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 8209a457a..4558aefcb 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1273,17 +1273,38 @@ internal partial class PluginManager : IDisposable, IServiceType { Log.Information($"Loading dev plugin {name}"); var devPlugin = new LocalDevPlugin(dllFile, manifest); - loadPlugin &= !isBoot || devPlugin.StartOnBoot; + loadPlugin &= !isBoot; var probablyInternalNameForThisPurpose = manifest?.InternalName ?? dllFile.Name; + var wantsInDefaultProfile = this.profileManager.DefaultProfile.WantsPlugin(probablyInternalNameForThisPurpose); - if (wantsInDefaultProfile == false && devPlugin.StartOnBoot) + var wantsInAnyProfile = this.profileManager.GetWantState(probablyInternalNameForThisPurpose, false, false); + 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. + if (!wantsInAnyProfile) + loadPlugin = false; + } + 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. + this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, false, false); + } + else if (wantsInDefaultProfile == true && devPlugin.StartOnBoot) + { + // We wanted this plugin, and StartOnBoot is on. That means we actually do want it. this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, true, false); } else if (wantsInDefaultProfile == true && !devPlugin.StartOnBoot) { + // We wanted this plugin, but StartOnBoot is off. This means we don't want it anymore. + this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, false, false); + } + else if (wantsInDefaultProfile == false && !devPlugin.StartOnBoot) + { + // We didn't want this plugin, and StartOnBoot is off. We don't want it. this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, false, false); } From 2e2a63b89e178d606ed22a13bb5e8f5e8590a78d Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 20 Jun 2023 20:35:00 +0200 Subject: [PATCH 11/28] fix: preserve legacy disabled state when loading a devplugin for the first time --- Dalamud/Plugin/Internal/PluginManager.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 4558aefcb..e3c442473 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -694,6 +694,8 @@ internal partial class PluginManager : IDisposable, IServiceType { if (!setting.IsEnabled) continue; + + Log.Verbose("Scanning dev plugins at {Path}", setting.Path); if (Directory.Exists(setting.Path)) { @@ -1279,33 +1281,38 @@ internal partial class PluginManager : IDisposable, IServiceType var wantsInDefaultProfile = this.profileManager.DefaultProfile.WantsPlugin(probablyInternalNameForThisPurpose); - var wantsInAnyProfile = this.profileManager.GetWantState(probablyInternalNameForThisPurpose, false, false); 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. - if (!wantsInAnyProfile) - loadPlugin = false; } 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); this.profileManager.DefaultProfile.AddOrUpdate(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); this.profileManager.DefaultProfile.AddOrUpdate(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); this.profileManager.DefaultProfile.AddOrUpdate(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); this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, false, false); + loadPlugin = false; } plugin = devPlugin; From da8bbf5a282f2666bf7122b15a9141e07034873a Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 20 Jun 2023 20:38:57 +0200 Subject: [PATCH 12/28] build: 7.7.0.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index c52065616..358ebcb66 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.6.5.0 + 7.7.0.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From c3fe41640e323efa021d9efeeb00b556fe6d19b6 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 20 Jun 2023 21:55:31 +0200 Subject: [PATCH 13/28] fix: prevent some deadlocks in profile management code We can never load/unload plugins synchronously, since they might need to get back on framework thread to unload. Also fixes an issue wherein applying might have gotten stuck if an unload threw an exception. --- .../PluginInstaller/PluginInstallerWindow.cs | 25 ++++--- .../PluginInstaller/ProfileManagerWidget.cs | 23 +++--- Dalamud/Plugin/Internal/PluginManager.cs | 11 +-- Dalamud/Plugin/Internal/Profiles/Profile.cs | 70 +++++++++++-------- .../Profiles/ProfileCommandHandler.cs | 8 +-- .../Internal/Profiles/ProfileManager.cs | 56 +++++++-------- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 2 +- 7 files changed, 107 insertions(+), 88 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index be27768be..7a850f5df 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2367,12 +2367,12 @@ internal class PluginInstallerWindow : Window, IDisposable { if (inProfile) { - Task.Run(() => profile.AddOrUpdate(plugin.Manifest.InternalName, true)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.InternalName, true)) .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotAdd); } else { - Task.Run(() => profile.Remove(plugin.Manifest.InternalName)) + Task.Run(() => profile.RemoveAsync(plugin.Manifest.InternalName)) .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotRemove); } } @@ -2391,14 +2391,17 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGuiComponents.IconButton(FontAwesomeIcon.Times)) { - profileManager.DefaultProfile.AddOrUpdate(plugin.Manifest.InternalName, plugin.IsLoaded, false); + // TODO: Work this out + Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(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))) { - profile.Remove(plugin.Manifest.InternalName, false); + Task.Run(() => profile.RemoveAsync(plugin.Manifest.InternalName, false)) + .GetAwaiter().GetResult(); } - // TODO error handling - Task.Run(() => profileManager.ApplyAllWantStates()); + Task.Run(profileManager.ApplyAllWantStatesAsync) + .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_ProfileApplyFail); } ImGui.SameLine(); @@ -2448,7 +2451,9 @@ internal class PluginInstallerWindow : Window, IDisposable return; } - profileManager.DefaultProfile.AddOrUpdate(plugin.Manifest.InternalName, false, false); + // TODO: Work this out + Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, false, false)) + .GetAwaiter().GetResult(); this.enableDisableStatus = OperationStatus.Complete; notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success); @@ -2466,7 +2471,9 @@ internal class PluginInstallerWindow : Window, IDisposable plugin.ReloadManifest(); } - profileManager.DefaultProfile.AddOrUpdate(plugin.Manifest.InternalName, true, false); + // TODO: Work this out + Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false)) + .GetAwaiter().GetResult(); var loadTask = Task.Run(() => plugin.LoadAsync(PluginLoadReason.Installer)) .ContinueWith( @@ -3282,6 +3289,8 @@ internal class PluginInstallerWindow : Window, IDisposable public static string ErrorModal_UpdaterFatal => Loc.Localize("InstallerUpdaterFatal", "Failed to update plugins.\nPlease restart your game and try again. If this error occurs again, please complain."); + public static string ErrorModal_ProfileApplyFail => Loc.Localize("InstallerProfileApplyFail", "Failed to process collections.\nPlease restart your game and try again. If this error occurs again, please complain."); + public static string ErrorModal_UpdaterFail(int failCount) => Loc.Localize("InstallerUpdaterFail", "Failed to update {0} plugins.\nPlease restart your game and try again. If this error occurs again, please complain.").Format(failCount); public static string ErrorModal_UpdaterFailPartial(int successCount, int failCount) => Loc.Localize("InstallerUpdaterFailPartial", "Updated {0} plugins, failed to update {1}.\nPlease restart your game and try again. If this error occurs again, please complain.").Format(successCount, failCount); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 4f92cebb8..df2ec5ce6 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -119,7 +119,7 @@ internal class ProfileManagerWidget var isEnabled = profile.IsEnabled; if (ImGuiComponents.ToggleButton($"###toggleButton{profile.Guid}", ref isEnabled)) { - Task.Run(() => profile.SetState(isEnabled)) + Task.Run(() => profile.SetStateAsync(isEnabled)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } @@ -228,9 +228,7 @@ internal class ProfileManagerWidget if (ImGui.Selectable($"{plugin.Manifest.Name}###selector{plugin.Manifest.InternalName}")) { - // TODO this sucks - profile.AddOrUpdate(plugin.Manifest.InternalName, true, false); - Task.Run(() => profman.ApplyAllWantStates()) + Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } } @@ -273,8 +271,12 @@ internal class ProfileManagerWidget this.Reset(); // DeleteProfile() is sync, it doesn't apply and we are modifying the plugins collection. Will throw below when iterating - profman.DeleteProfile(profile); - Task.Run(() => profman.ApplyAllWantStates()) + // TODO: DeleteProfileAsync should probably apply as well + Task.Run(async () => + { + await profman.DeleteProfileAsync(profile); + await profman.ApplyAllWantStatesAsync(); + }) .ContinueWith(t => { this.installer.DisplayErrorContinuation(t, Locs.ErrorCouldNotChangeState); @@ -300,7 +302,7 @@ internal class ProfileManagerWidget var isEnabled = profile.IsEnabled; if (ImGuiComponents.ToggleButton($"###toggleButton{profile.Guid}", ref isEnabled)) { - Task.Run(() => profile.SetState(isEnabled)) + Task.Run(() => profile.SetStateAsync(isEnabled)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } @@ -391,7 +393,7 @@ internal class ProfileManagerWidget var enabled = plugin.IsEnabled; if (ImGui.Checkbox($"###{this.editingProfileGuid}-{plugin.InternalName}", ref enabled)) { - Task.Run(() => profile.AddOrUpdate(plugin.InternalName, enabled)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.InternalName, enabled)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } @@ -411,9 +413,8 @@ internal class ProfileManagerWidget if (wantRemovePluginInternalName != null) { // TODO: handle error - profile.Remove(wantRemovePluginInternalName, false); - Task.Run(() => profman.ApplyAllWantStates()) - .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotRemove); + Task.Run(() => profile.RemoveAsync(wantRemovePluginInternalName, false)) + .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotRemove); } if (!didAny) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index e3c442473..9b407d724 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -262,6 +262,7 @@ internal partial class PluginManager : IDisposable, IServiceType /// /// Get a disposable that will lock plugin lists while it is not disposed. + /// You must NEVER use this in async code. /// /// The aforementioned disposable. public IDisposable GetSyncScope() => new ScopedSyncRoot(this.pluginListLock); @@ -1290,28 +1291,28 @@ internal partial class PluginManager : IDisposable, IServiceType { // 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); - this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, false, false); + 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); - this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, true, false); + 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); - this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, false, false); + 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); - this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, false, false); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false); loadPlugin = false; } @@ -1328,7 +1329,7 @@ internal partial class PluginManager : IDisposable, IServiceType #pragma warning restore CS0618 // Need to do this here, so plugins that don't load are still added to the default profile - var wantToLoad = this.profileManager.GetWantState(plugin.Manifest.InternalName, defaultState); + var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.InternalName, defaultState); if (loadPlugin) { diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 4921db42b..29ab64fa7 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Logging.Internal; @@ -115,7 +116,8 @@ internal class Profile /// Whether or not the profile is enabled. /// Whether or not the current state should immediately be applied. /// Thrown when an untoggleable profile is toggled. - public void SetState(bool enabled, bool apply = true) + /// A representing the asynchronous operation. + public async Task SetStateAsync(bool enabled, bool apply = true) { if (this.IsDefaultProfile) throw new InvalidOperationException("Cannot set state of default profile"); @@ -127,7 +129,7 @@ internal class Profile Service.Get().QueueSave(); if (apply) - this.manager.ApplyAllWantStates(); + await this.manager.ApplyAllWantStatesAsync(); } /// @@ -137,10 +139,11 @@ internal class Profile /// 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. public bool? WantsPlugin(string internalName) { - using var lockScope = this.manager.GetSyncScope(); - - var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); - return entry?.IsEnabled; + lock (this) + { + var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); + return entry?.IsEnabled; + } } /// @@ -150,36 +153,38 @@ internal class Profile /// The internal name of the plugin. /// Whether or not the plugin should be enabled. /// Whether or not the current state should immediately be applied. - public void AddOrUpdate(string internalName, bool state, bool apply = true) + /// A representing the asynchronous operation. + public async Task AddOrUpdateAsync(string internalName, bool state, bool apply = true) { - using var lockScope = this.manager.GetSyncScope(); - Debug.Assert(!internalName.IsNullOrEmpty(), "!internalName.IsNullOrEmpty()"); - var existing = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); - if (existing != null) + lock (this) { - existing.IsEnabled = state; - } - else - { - this.modelV1.Plugins.Add(new ProfileModelV1.ProfileModelV1Plugin + var existing = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); + if (existing != null) { - InternalName = internalName, - IsEnabled = state, - }); + existing.IsEnabled = state; + } + else + { + this.modelV1.Plugins.Add(new ProfileModelV1.ProfileModelV1Plugin + { + InternalName = internalName, + 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) { - this.manager.DefaultProfile.Remove(internalName, false); + await this.manager.DefaultProfile.RemoveAsync(internalName, false); } Service.Get().QueueSave(); if (apply) - this.manager.ApplyAllWantStates(); + await this.manager.ApplyAllWantStatesAsync(); } /// @@ -188,23 +193,26 @@ internal class Profile /// /// The internal name of the plugin. /// Whether or not the current state should immediately be applied. - public void Remove(string internalName, bool apply = true) + /// A representing the asynchronous operation. + public async Task RemoveAsync(string internalName, bool apply = true) { - using var lockScope = this.manager.GetSyncScope(); - - var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); - if (entry == null) - throw new ArgumentException($"No plugin \"{internalName}\" in profile \"{this.Guid}\""); + ProfileModelV1.ProfileModelV1Plugin entry; + lock (this) + { + entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); + if (entry == null) + throw new ArgumentException($"No plugin \"{internalName}\" in profile \"{this.Guid}\""); - if (!this.modelV1.Plugins.Remove(entry)) - throw new Exception("Couldn't remove plugin from model collection"); + 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(internalName)) { if (!this.IsDefaultProfile) { - this.manager.DefaultProfile.AddOrUpdate(internalName, entry.IsEnabled, false); + await this.manager.DefaultProfile.AddOrUpdateAsync(internalName, entry.IsEnabled, false); } else { @@ -215,6 +223,6 @@ internal class Profile Service.Get().QueueSave(); if (apply) - this.manager.ApplyAllWantStates(); + await this.manager.ApplyAllWantStatesAsync(); } } diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs index 81e63e0bc..8ea55856c 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs @@ -96,14 +96,14 @@ internal class ProfileCommandHandler : IServiceType, IDisposable { case ProfileOp.Enable: if (!profile.IsEnabled) - profile.SetState(true, false); + Task.Run(() => profile.SetStateAsync(true, false)).GetAwaiter().GetResult(); break; case ProfileOp.Disable: if (profile.IsEnabled) - profile.SetState(false, false); + Task.Run(() => profile.SetStateAsync(false, false)).GetAwaiter().GetResult(); break; case ProfileOp.Toggle: - profile.SetState(!profile.IsEnabled, false); + Task.Run(() => profile.SetStateAsync(!profile.IsEnabled, false)).GetAwaiter().GetResult(); break; default: throw new ArgumentOutOfRangeException(); @@ -118,7 +118,7 @@ internal class ProfileCommandHandler : IServiceType, IDisposable this.chat.Print(Loc.Localize("ProfileCommandsDisabling", "Disabling collection \"{0}\"...").Format(profile.Name)); } - Task.Run(() => this.profileManager.ApplyAllWantStates()).ContinueWith(t => + Task.Run(this.profileManager.ApplyAllWantStatesAsync).ContinueWith(t => { if (!t.IsCompletedSuccessfully && t.Exception != null) { diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 6250805b0..a3601c773 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; @@ -60,12 +59,6 @@ internal class ProfileManager : IServiceType /// public bool IsBusy => this.isBusy; - /// - /// Get a disposable that will lock the profile list while it is not disposed. - /// - /// The aforementioned disposable. - public ScopedSyncRoot GetSyncScope() => new ScopedSyncRoot(this.profiles); - /// /// Check if any enabled profile wants a specific plugin enabled. /// @@ -73,13 +66,13 @@ internal class ProfileManager : IServiceType /// The state the plugin shall be in, if it needs to be added. /// Whether or not the plugin should be added to the default preset, if it's not present in any preset. /// Whether or not the plugin shall be enabled. - public bool GetWantState(string internalName, bool defaultState, bool addIfNotDeclared = true) + public async Task GetWantStateAsync(string internalName, bool defaultState, bool addIfNotDeclared = true) { + var want = false; + var wasInAnyProfile = false; + lock (this.profiles) { - var want = false; - var wasInAnyProfile = false; - foreach (var profile in this.profiles) { var state = profile.WantsPlugin(internalName); @@ -89,16 +82,17 @@ internal class ProfileManager : IServiceType wasInAnyProfile = true; } } - - if (!wasInAnyProfile && addIfNotDeclared) - { - Log.Warning("{Name} was not in any profile, adding to default with {Default}", internalName, defaultState); - this.DefaultProfile.AddOrUpdate(internalName, defaultState, false); - return defaultState; - } - - return want; } + + 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); + + return defaultState; + } + + return want; } /// @@ -186,20 +180,24 @@ internal class ProfileManager : IServiceType /// Go through all profiles and plugins, and enable/disable plugins they want active. /// This will block until all plugins have been loaded/reloaded. /// - public void ApplyAllWantStates() + /// A representing the asynchronous operation. + public async Task ApplyAllWantStatesAsync() { - var pm = Service.Get(); - using var managerLock = pm.GetSyncScope(); - using var profilesLock = this.GetSyncScope(); + if (this.isBusy) + throw new Exception("Already busy, this must not run in parallel. Check before starting another apply!"); this.isBusy = true; Log.Information("Getting want states..."); - var wantActive = this.profiles + List wantActive; + lock (this.profiles) + { + wantActive = this.profiles .Where(x => x.IsEnabled) .SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled) .Select(plugin => plugin.InternalName)) .Distinct().ToList(); + } foreach (var internalName in wantActive) { @@ -210,6 +208,7 @@ internal class ProfileManager : IServiceType var tasks = new List(); + var pm = Service.Get(); foreach (var installedPlugin in pm.InstalledPlugins) { var wantThis = wantActive.Contains(installedPlugin.Manifest.InternalName); @@ -237,7 +236,7 @@ internal class ProfileManager : IServiceType // This is probably not ideal... Might need to rethink the error handling strategy for this. try { - Task.WaitAll(tasks.ToArray()); + await Task.WhenAll(tasks.ToArray()); } catch (Exception e) { @@ -255,12 +254,13 @@ internal class ProfileManager : IServiceType /// You should definitely apply states after this. It doesn't do it for you. /// /// The profile to delete. - public void DeleteProfile(Profile profile) + /// A representing the asynchronous operation. + public async Task DeleteProfileAsync(Profile profile) { // 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()) { - profile.Remove(plugin.InternalName, false); + await profile.RemoveAsync(plugin.InternalName, false); } if (!this.config.SavedProfiles!.Remove(profile.Model)) diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 1af2165da..14ae4a0c0 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -213,7 +213,7 @@ internal class LocalPlugin : IDisposable /// INCLUDES the default profile. /// public bool IsWantedByAnyProfile => - Service.Get().GetWantState(this.Manifest.InternalName, false, false); + Service.Get().GetWantStateAsync(this.Manifest.InternalName, false, false).GetAwaiter().GetResult(); /// /// Gets a value indicating whether this plugin's API level is out of date. From a79049e24f72494c09d1b87439768cfe71d7425f Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 20 Jun 2023 22:33:22 +0200 Subject: [PATCH 14/28] chore: catch some exceptions when drawing installer categories --- .../PluginInstaller/PluginInstallerWindow.cs | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 7a850f5df..cc2c45089 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1115,20 +1115,35 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.PushStyleVar(ImGuiStyleVar.CellPadding, ImGuiHelpers.ScaledVector2(5, 0)); if (ImGui.BeginTable("##InstallerCategoriesCont", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV)) { - ImGui.TableSetupColumn("##InstallerCategoriesSelector", ImGuiTableColumnFlags.WidthFixed, useMenuWidth * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("##InstallerCategoriesBody", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableNextRow(); - - ImGui.TableNextColumn(); - this.DrawPluginCategorySelectors(); - - ImGui.TableNextColumn(); - if (ImGui.BeginChild("ScrollingPlugins", new Vector2(-1, 0), false, ImGuiWindowFlags.NoBackground)) + try { - this.DrawPluginCategoryContent(); - } + ImGui.TableSetupColumn("##InstallerCategoriesSelector", ImGuiTableColumnFlags.WidthFixed, useMenuWidth * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##InstallerCategoriesBody", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableNextRow(); - ImGui.EndChild(); + ImGui.TableNextColumn(); + this.DrawPluginCategorySelectors(); + + ImGui.TableNextColumn(); + if (ImGui.BeginChild("ScrollingPlugins", new Vector2(-1, 0), false, ImGuiWindowFlags.NoBackground)) + { + try + { + this.DrawPluginCategoryContent(); + } + catch (Exception ex) + { + Log.Error(ex, "Could not draw category content"); + } + } + + ImGui.EndChild(); + } + catch (Exception ex) + { + Log.Error(ex, "Could not draw plugin categories"); + } + ImGui.EndTable(); } From 5d5af70e21873eea869699a03669c84686e38358 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 20 Jun 2023 22:34:04 +0200 Subject: [PATCH 15/28] build: 7.7.1.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 358ebcb66..b0dc21f51 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.7.0.0 + 7.7.1.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From a954e839764f00973268c1f4207db1efbb2256f5 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 20 Jun 2023 22:47:29 +0200 Subject: [PATCH 16/28] fix: PM.ReposReady now also includes the time needed for filtering --- Dalamud/Plugin/Internal/PluginManager.cs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 9b407d724..ccb54cc99 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -202,7 +202,7 @@ internal partial class PluginManager : IDisposable, IServiceType /// /// Gets a value indicating whether all added repos are not in progress. /// - public bool ReposReady => this.Repos.All(repo => repo.State != PluginRepositoryState.InProgress); + public bool ReposReady { get; private set; } /// /// Gets a value indicating whether the plugin manager started in safe mode. @@ -649,15 +649,27 @@ internal partial class PluginManager : IDisposable, IServiceType public async Task ReloadPluginMastersAsync(bool notify = true) { Log.Information("Now reloading all PluginMasters..."); + this.ReposReady = false; - Debug.Assert(!this.Repos.First().IsThirdParty, "First repository should be main repository"); - await this.Repos.First().ReloadPluginMasterAsync(); // Load official repo first + try + { + Debug.Assert(!this.Repos.First().IsThirdParty, "First repository should be main repository"); + await this.Repos.First().ReloadPluginMasterAsync(); // Load official repo first - await Task.WhenAll(this.Repos.Skip(1).Select(repo => repo.ReloadPluginMasterAsync())); + await Task.WhenAll(this.Repos.Skip(1).Select(repo => repo.ReloadPluginMasterAsync())); - Log.Information("PluginMasters reloaded, now refiltering..."); + Log.Information("PluginMasters reloaded, now refiltering..."); - this.RefilterPluginMasters(notify); + this.RefilterPluginMasters(notify); + } + catch (Exception ex) + { + Log.Error(ex, "Could not reload plugin repositories"); + } + finally + { + this.ReposReady = true; + } } /// From c68793b5894f397741811d383be6468f8dc6d8ac Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 21 Jun 2023 19:13:33 +0200 Subject: [PATCH 17/28] fix: use ImRaii for PI DrawPluginCategories Prevents mismatched BeginTable/EndTable --- .../PluginInstaller/PluginInstallerWindow.cs | 59 +++++++++---------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index cc2c45089..2c28b962c 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1110,45 +1110,40 @@ internal class PluginInstallerWindow : Window, IDisposable var useContentWidth = ImGui.GetContentRegionAvail().X; - if (ImGui.BeginChild("InstallerCategories", new Vector2(useContentWidth, useContentHeight * ImGuiHelpers.GlobalScale))) + using (ImRaii.Child("InstallerCategories", new Vector2(useContentWidth, useContentHeight * ImGuiHelpers.GlobalScale))) { - ImGui.PushStyleVar(ImGuiStyleVar.CellPadding, ImGuiHelpers.ScaledVector2(5, 0)); - if (ImGui.BeginTable("##InstallerCategoriesCont", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV)) + using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, ImGuiHelpers.ScaledVector2(5, 0)); + using var table = ImRaii.Table( + "##InstallerCategoriesCont", + 2, + ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV); + + try { - try + ImGui.TableSetupColumn("##InstallerCategoriesSelector", ImGuiTableColumnFlags.WidthFixed, useMenuWidth * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##InstallerCategoriesBody", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + this.DrawPluginCategorySelectors(); + + ImGui.TableNextColumn(); + using (ImRaii.Child("ScrollingPlugins", new Vector2(-1, 0), false, ImGuiWindowFlags.NoBackground)) { - ImGui.TableSetupColumn("##InstallerCategoriesSelector", ImGuiTableColumnFlags.WidthFixed, useMenuWidth * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("##InstallerCategoriesBody", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableNextRow(); - - ImGui.TableNextColumn(); - this.DrawPluginCategorySelectors(); - - ImGui.TableNextColumn(); - if (ImGui.BeginChild("ScrollingPlugins", new Vector2(-1, 0), false, ImGuiWindowFlags.NoBackground)) + try { - try - { - this.DrawPluginCategoryContent(); - } - catch (Exception ex) - { - Log.Error(ex, "Could not draw category content"); - } + this.DrawPluginCategoryContent(); + } + catch (Exception ex) + { + Log.Error(ex, "Could not draw category content"); } - - ImGui.EndChild(); } - catch (Exception ex) - { - Log.Error(ex, "Could not draw plugin categories"); - } - - ImGui.EndTable(); } - - ImGui.PopStyleVar(); - ImGui.EndChild(); + catch (Exception ex) + { + Log.Error(ex, "Could not draw plugin categories"); + } } } From 00fd9079a92f8ff128a0400517561106fbf67c81 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 21 Jun 2023 19:33:52 +0200 Subject: [PATCH 18/28] fix: sync profile/plugin lists when drawing profile manager --- .../Windows/PluginInstaller/ProfileManagerWidget.cs | 4 +++- Dalamud/Plugin/Internal/Profiles/Profile.cs | 7 +++++++ Dalamud/Plugin/Internal/Profiles/ProfileManager.cs | 7 +++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index df2ec5ce6..8ec6be867 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -111,6 +111,7 @@ internal class ProfileManagerWidget { Guid? toCloneGuid = null; + using var syncScope = profman.GetSyncScope(); foreach (var profile in profman.Profiles) { if (profile.IsDefaultProfile) @@ -329,7 +330,8 @@ internal class ProfileManagerWidget var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale; string? wantRemovePluginInternalName = null; - foreach (var plugin in profile.Plugins) + using var syncScope = profile.GetSyncScope(); + foreach (var plugin in profile.Plugins.ToArray()) { didAny = true; var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == plugin.InternalName); diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 29ab64fa7..71feff0c2 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -109,6 +109,13 @@ internal class Profile /// public ProfileModel Model => this.modelV1; + /// + /// Get a disposable that will lock the plugin list while it is not disposed. + /// You must NEVER use this in async code. + /// + /// The aforementioned disposable. + public IDisposable GetSyncScope() => new ScopedSyncRoot(this); + /// /// Set this profile's state. This cannot be called for the default profile. /// This will block until all states have been applied. diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index a3601c773..46b572c1a 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -58,6 +58,13 @@ internal class ProfileManager : IServiceType /// Gets a value indicating whether or not the profile manager is busy enabling/disabling plugins. /// public bool IsBusy => this.isBusy; + + /// + /// Get a disposable that will lock the profile list while it is not disposed. + /// You must NEVER use this in async code. + /// + /// The aforementioned disposable. + public IDisposable GetSyncScope() => new ScopedSyncRoot(this.profiles); /// /// Check if any enabled profile wants a specific plugin enabled. From 30176e61f5962c5a42c1ebc0d1800e9d0661f1c6 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 21 Jun 2023 20:30:34 +0200 Subject: [PATCH 19/28] fix: use ImRaii conditions properly --- .../PluginInstaller/PluginInstallerWindow.cs | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 2c28b962c..1e62d4742 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1110,39 +1110,46 @@ internal class PluginInstallerWindow : Window, IDisposable var useContentWidth = ImGui.GetContentRegionAvail().X; - using (ImRaii.Child("InstallerCategories", new Vector2(useContentWidth, useContentHeight * ImGuiHelpers.GlobalScale))) + using var categoriesChild = ImRaii.Child("InstallerCategories", new Vector2(useContentWidth, useContentHeight * ImGuiHelpers.GlobalScale)); + if (categoriesChild) { using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, ImGuiHelpers.ScaledVector2(5, 0)); using var table = ImRaii.Table( "##InstallerCategoriesCont", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV); - - try + + if (table) { - ImGui.TableSetupColumn("##InstallerCategoriesSelector", ImGuiTableColumnFlags.WidthFixed, useMenuWidth * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("##InstallerCategoriesBody", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableNextRow(); - - ImGui.TableNextColumn(); - this.DrawPluginCategorySelectors(); - - ImGui.TableNextColumn(); - using (ImRaii.Child("ScrollingPlugins", new Vector2(-1, 0), false, ImGuiWindowFlags.NoBackground)) + try { - try + ImGui.TableSetupColumn("##InstallerCategoriesSelector", ImGuiTableColumnFlags.WidthFixed, useMenuWidth * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##InstallerCategoriesBody", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + this.DrawPluginCategorySelectors(); + + ImGui.TableNextColumn(); + + using var scrollingChild = + ImRaii.Child("ScrollingPlugins", new Vector2(-1, 0), false, ImGuiWindowFlags.NoBackground); + if (scrollingChild) { - this.DrawPluginCategoryContent(); - } - catch (Exception ex) - { - Log.Error(ex, "Could not draw category content"); + try + { + this.DrawPluginCategoryContent(); + } + catch (Exception ex) + { + Log.Error(ex, "Could not draw category content"); + } } } - } - catch (Exception ex) - { - Log.Error(ex, "Could not draw plugin categories"); + catch (Exception ex) + { + Log.Error(ex, "Could not draw plugin categories"); + } } } } @@ -2363,7 +2370,7 @@ internal class PluginInstallerWindow : Window, IDisposable var isLoadedAndUnloadable = plugin.State == PluginState.Loaded || plugin.State == PluginState.DependencyResolutionFailed; - StyleModelV1.DalamudStandard.Push(); + //StyleModelV1.DalamudStandard.Push(); var profileChooserPopupName = $"###pluginProfileChooser{plugin.Manifest.InternalName}"; if (ImGui.BeginPopup(profileChooserPopupName)) @@ -2526,7 +2533,7 @@ internal class PluginInstallerWindow : Window, IDisposable } } - StyleModelV1.DalamudStandard.Pop(); + //StyleModelV1.DalamudStandard.Pop(); ImGui.SameLine(); ImGuiHelpers.ScaledDummy(15, 0); From 5a76f3ebe5a4f165ca795df0f1f7eaf0c115de8f Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 21 Jun 2023 21:42:00 +0200 Subject: [PATCH 20/28] fix: don't use tables for the installer layout, more ImRaii conversion Apparently children inside tables is UB --- .../PluginInstaller/PluginInstallerWindow.cs | 61 ++++++++----------- .../PluginInstaller/ProfileManagerWidget.cs | 10 ++- 2 files changed, 31 insertions(+), 40 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 1e62d4742..ba249e051 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -392,7 +392,8 @@ internal class PluginInstallerWindow : Window, IDisposable var windowSize = ImGui.GetWindowSize(); var titleHeight = ImGui.GetFontSize() + (ImGui.GetStyle().FramePadding.Y * 2); - if (ImGui.BeginChild("###installerLoadingFrame", new Vector2(-1, -1), false)) + using var loadingChild = ImRaii.Child("###installerLoadingFrame", new Vector2(-1, -1), false); + if (loadingChild) { ImGui.GetWindowDrawList().PushClipRectFullScreen(); ImGui.GetWindowDrawList().AddRectFilled( @@ -480,8 +481,6 @@ internal class PluginInstallerWindow : Window, IDisposable ImGuiHelpers.CenteredText("One of your plugins may be blocking the installer."); ImGui.PopStyleColor(); } - - ImGui.EndChild(); } } @@ -1110,47 +1109,41 @@ internal class PluginInstallerWindow : Window, IDisposable var useContentWidth = ImGui.GetContentRegionAvail().X; - using var categoriesChild = ImRaii.Child("InstallerCategories", new Vector2(useContentWidth, useContentHeight * ImGuiHelpers.GlobalScale)); - if (categoriesChild) + using var installerMainChild = ImRaii.Child("InstallerCategories", new Vector2(useContentWidth, useContentHeight * ImGuiHelpers.GlobalScale)); + if (installerMainChild) { using var style = ImRaii.PushStyle(ImGuiStyleVar.CellPadding, ImGuiHelpers.ScaledVector2(5, 0)); - using var table = ImRaii.Table( - "##InstallerCategoriesCont", - 2, - ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV); - - if (table) + + try { - try + using (var categoriesChild = ImRaii.Child("InstallerCategoriesSelector", new Vector2(useMenuWidth * ImGuiHelpers.GlobalScale, -1), false)) { - ImGui.TableSetupColumn("##InstallerCategoriesSelector", ImGuiTableColumnFlags.WidthFixed, useMenuWidth * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("##InstallerCategoriesBody", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableNextRow(); - - ImGui.TableNextColumn(); - this.DrawPluginCategorySelectors(); - - ImGui.TableNextColumn(); - - using var scrollingChild = - ImRaii.Child("ScrollingPlugins", new Vector2(-1, 0), false, ImGuiWindowFlags.NoBackground); - if (scrollingChild) + if (categoriesChild) { - try - { - this.DrawPluginCategoryContent(); - } - catch (Exception ex) - { - Log.Error(ex, "Could not draw category content"); - } + this.DrawPluginCategorySelectors(); } } - catch (Exception ex) + + ImGui.SameLine(); + + using var scrollingChild = + ImRaii.Child("ScrollingPlugins", new Vector2(-1, -1), false, ImGuiWindowFlags.NoBackground); + if (scrollingChild) { - Log.Error(ex, "Could not draw plugin categories"); + try + { + this.DrawPluginCategoryContent(); + } + catch (Exception ex) + { + Log.Error(ex, "Could not draw category content"); + } } } + catch (Exception ex) + { + Log.Error(ex, "Could not draw plugin categories"); + } } } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 8ec6be867..ca3ca34b0 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -107,7 +107,8 @@ internal class ProfileManagerWidget var windowSize = ImGui.GetWindowSize(); - if (ImGui.BeginChild("###profileChooserScrolling")) + using var profileChooserChild = ImRaii.Child("###profileChooserScrolling"); + if (profileChooserChild) { Guid? toCloneGuid = null; @@ -180,8 +181,6 @@ internal class ProfileManagerWidget ImGuiHelpers.CenteredText(Locs.AddProfileHint); ImGui.PopStyleColor(); } - - ImGui.EndChild(); } } @@ -325,7 +324,8 @@ internal class ProfileManagerWidget ImGui.Separator(); var wantPluginAddPopup = false; - if (ImGui.BeginChild("###profileEditorPluginList")) + using var pluginListChild = ImRaii.Child("###profileEditorPluginList"); + if (pluginListChild) { var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale; string? wantRemovePluginInternalName = null; @@ -439,8 +439,6 @@ internal class ProfileManagerWidget ImGui.TextUnformatted(addPluginsText); ImGuiHelpers.ScaledDummy(10); - - ImGui.EndChild(); } if (wantPluginAddPopup) From 27b2e8371dfaccfc5154197edb5e9875043abca2 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 21 Jun 2023 21:42:28 +0200 Subject: [PATCH 21/28] build: 7.7.2.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index b0dc21f51..d0ac3afae 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.7.1.0 + 7.7.2.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 11ea64410ee245a852c1aaca3a8f87bd8d489bae Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 23 Jun 2023 11:39:52 +0200 Subject: [PATCH 22/28] fix: use explicit ID for add plugin popup --- .../Internal/Windows/PluginInstaller/ProfileManagerWidget.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index ca3ca34b0..b43d70e7d 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -206,6 +206,7 @@ internal class ProfileManagerWidget } const string addPluginToProfilePopup = "###addPluginToProfile"; + var addPluginToProfilePopupId = ImGui.GetID(addPluginToProfilePopup); using (var popup = ImRaii.Popup(addPluginToProfilePopup)) { if (popup.Success) @@ -444,7 +445,7 @@ internal class ProfileManagerWidget if (wantPluginAddPopup) { this.pickerSearch = string.Empty; - ImGui.OpenPopup(addPluginToProfilePopup); + ImGui.OpenPopup(addPluginToProfilePopupId); } } From 694159a5103371e8edd78b9356e2bbcbad149a30 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 23 Jun 2023 10:54:40 -0700 Subject: [PATCH 23/28] /xldata window refactor (#1257) Reworks the `/xldata` window so that each individual section is in its own file. --- .../Interface/Internal/DalamudInterface.cs | 1 + Dalamud/Interface/Internal/UiDebug.cs | 10 +- .../Internal/Windows/Data/DataKindEnum.cs | 158 ++ .../Internal/Windows/Data/DataWindow.cs | 191 ++ .../Windows/Data/IDataWindowWidget.cs | 27 + .../Data/Widgets/AddonInspectorWidget.cs | 32 + .../Windows/Data/Widgets/AddonWidget.cs | 66 + .../Windows/Data/Widgets/AddressesWidget.cs | 64 + .../Windows/Data/Widgets/AetherytesWidget.cs | 87 + .../Data/Widgets/AtkArrayDataBrowserWidget.cs | 173 ++ .../Windows/Data/Widgets/BuddyListWidget.cs | 110 + .../Windows/Data/Widgets/CommandWidget.cs | 33 + .../Windows/Data/Widgets/ConditionWidget.cs | 52 + .../Data/Widgets/ConfigurationWidget.cs | 29 + .../Windows/Data/Widgets/DataShareWidget.cs | 53 + .../Windows/Data/Widgets/DtrBarWidget.cs | 80 + .../Windows/Data/Widgets/FateTableWidget.cs | 68 + .../Windows/Data/Widgets/FlyTextWidget.cs | 77 + .../Data/Widgets/FontAwesomeTestWidget.cs | 78 + .../Windows/Data/Widgets/GamepadWidget.cs | 86 + .../Windows/Data/Widgets/GaugeWidget.cs | 72 + .../Windows/Data/Widgets/HookWidget.cs | 87 + .../Windows/Data/Widgets/ImGuiWidget.cs | 74 + .../Windows/Data/Widgets/KeyStateWidget.cs | 50 + .../Windows/Data/Widgets/ObjectTableWidget.cs | 119 ++ .../Windows/Data/Widgets/PartyListWidget.cs | 63 + .../Windows/Data/Widgets/PluginIpcWidget.cs | 84 + .../Windows/Data/Widgets/SeFontTestWidget.cs | 33 + .../Data/Widgets/ServerOpcodeWidget.cs | 37 + .../Windows/Data/Widgets/StartInfoWidget.cs | 30 + .../Windows/Data/Widgets/TargetWidget.cs | 88 + .../Data/Widgets/TaskSchedulerWidget.cs | 256 +++ .../Windows/Data/Widgets/TexWidget.cs | 69 + .../Windows/Data/Widgets/ToastWidget.cs | 74 + .../Windows/Data/Widgets/UIColorWidget.cs | 60 + .../Interface/Internal/Windows/DataWindow.cs | 1886 ----------------- Dalamud/Utility/Util.cs | 37 +- 37 files changed, 2701 insertions(+), 1893 deletions(-) create mode 100644 Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/DataWindow.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs delete mode 100644 Dalamud/Interface/Internal/Windows/DataWindow.cs diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 202b394c6..479297c20 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -16,6 +16,7 @@ using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Windows; +using Dalamud.Interface.Internal.Windows.Data; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.SelfTest; using Dalamud.Interface.Internal.Windows.Settings; diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 3b3c4e003..d1e7a6b78 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -53,8 +53,6 @@ internal unsafe class UiDebug { } - private delegate AtkStage* GetAtkStageSingleton(); - /// /// Renders this window. /// @@ -165,7 +163,7 @@ internal unsafe class UiDebug private void PrintSimpleNode(AtkResNode* node, string treePrefix) { var popped = false; - var isVisible = (node->Flags & 0x10) == 0x10; + var isVisible = node->NodeFlags.HasFlag(NodeFlags.Visible); if (isVisible) ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(0, 255, 0, 255)); @@ -296,7 +294,7 @@ internal unsafe class UiDebug var compNode = (AtkComponentNode*)node; var popped = false; - var isVisible = (node->Flags & 0x10) == 0x10; + var isVisible = node->NodeFlags.HasFlag(NodeFlags.Visible); var componentInfo = compNode->Component->UldManager; @@ -396,7 +394,7 @@ internal unsafe class UiDebug ImGui.SameLine(); if (ImGui.SmallButton($"T:Visible##{(ulong)node:X}")) { - node->Flags ^= 0x10; + node->NodeFlags ^= NodeFlags.Visible; } ImGui.SameLine(); @@ -573,7 +571,7 @@ internal unsafe class UiDebug if (node == null) return false; while (node != null) { - if ((node->Flags & (short)NodeFlags.Visible) != (short)NodeFlags.Visible) return false; + if (!node->NodeFlags.HasFlag(NodeFlags.Visible)) return false; node = node->ParentNode; } diff --git a/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs b/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs new file mode 100644 index 000000000..99c6cb6e9 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs @@ -0,0 +1,158 @@ +// ReSharper disable InconsistentNaming // Naming is suppressed so we can replace '_' with ' ' +namespace Dalamud.Interface.Internal.Windows; + +/// +/// Enum representing a DataKind for the Data Window. +/// +internal enum DataKind +{ + /// + /// Server Opcode Display. + /// + Server_OpCode, + + /// + /// Address. + /// + Address, + + /// + /// Object Table. + /// + Object_Table, + + /// + /// Fate Table. + /// + Fate_Table, + + /// + /// SE Font Test. + /// + SE_Font_Test, + + /// + /// FontAwesome Test. + /// + FontAwesome_Test, + + /// + /// Party List. + /// + Party_List, + + /// + /// Buddy List. + /// + Buddy_List, + + /// + /// Plugin IPC Test. + /// + Plugin_IPC, + + /// + /// Player Condition. + /// + Condition, + + /// + /// Gauge. + /// + Gauge, + + /// + /// Command. + /// + Command, + + /// + /// Addon. + /// + Addon, + + /// + /// Addon Inspector. + /// + Addon_Inspector, + + /// + /// AtkArrayData Browser. + /// + AtkArrayData_Browser, + + /// + /// StartInfo. + /// + StartInfo, + + /// + /// Target. + /// + Target, + + /// + /// Toast. + /// + Toast, + + /// + /// Fly Text. + /// + FlyText, + + /// + /// ImGui. + /// + ImGui, + + /// + /// Tex. + /// + Tex, + + /// + /// KeyState. + /// + KeyState, + + /// + /// GamePad. + /// + Gamepad, + + /// + /// Configuration. + /// + Configuration, + + /// + /// Task Scheduler. + /// + TaskSched, + + /// + /// Hook. + /// + Hook, + + /// + /// Aetherytes. + /// + Aetherytes, + + /// + /// DTR Bar. + /// + Dtr_Bar, + + /// + /// UIColor. + /// + UIColor, + + /// + /// Data Share. + /// + DataShare, +} diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs new file mode 100644 index 000000000..f392d3912 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +using Dalamud.Game.Gui; +using Dalamud.Interface.Components; +using Dalamud.Interface.Windowing; +using ImGuiNET; +using Serilog; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Class responsible for drawing the data/debug window. +/// +internal class DataWindow : Window +{ + private readonly IDataWindowWidget[] modules = + { + new ServerOpcodeWidget(), + new AddressesWidget(), + new ObjectTableWidget(), + new FateTableWidget(), + new SeFontTestWidget(), + new FontAwesomeTestWidget(), + new PartyListWidget(), + new BuddyListWidget(), + new PluginIpcWidget(), + new ConditionWidget(), + new GaugeWidget(), + new CommandWidget(), + new AddonWidget(), + new AddonInspectorWidget(), + new AtkArrayDataBrowserWidget(), + new StartInfoWidget(), + new TargetWidget(), + new ToastWidget(), + new FlyTextWidget(), + new ImGuiWidget(), + new TexWidget(), + new KeyStateWidget(), + new GamepadWidget(), + new ConfigurationWidget(), + new TaskSchedulerWidget(), + new HookWidget(), + new AetherytesWidget(), + new DtrBarWidget(), + new UIColorWidget(), + new DataShareWidget(), + }; + + private readonly Dictionary dataKindNames = new(); + + private bool isExcept; + private DataKind currentKind; + + /// + /// Initializes a new instance of the class. + /// + public DataWindow() + : base("Dalamud Data") + { + this.Size = new Vector2(500, 500); + this.SizeCondition = ImGuiCond.FirstUseEver; + + this.RespectCloseHotkey = false; + + foreach (var dataKind in Enum.GetValues()) + { + this.dataKindNames[dataKind] = dataKind.ToString().Replace("_", " "); + } + + this.Load(); + } + + /// + public override void OnOpen() + { + } + + /// + public override void OnClose() + { + } + + /// + /// Set the DataKind dropdown menu. + /// + /// Data kind name, can be lower and/or without spaces. + public void SetDataKind(string dataKind) + { + if (string.IsNullOrEmpty(dataKind)) + return; + + dataKind = dataKind switch + { + "ai" => "Addon Inspector", + "at" => "Object Table", // Actor Table + "ot" => "Object Table", + "uic" => "UIColor", + _ => dataKind, + }; + + dataKind = dataKind.Replace(" ", string.Empty).ToLower(); + + var matched = Enum + .GetValues() + .FirstOrDefault(kind => Enum.GetName(kind)?.Replace("_", string.Empty).ToLower() == dataKind); + + if (matched != default) + { + this.currentKind = matched; + } + else + { + Service.Get().PrintError($"/xldata: Invalid data type {dataKind}"); + } + } + + /// + /// Draw the window via ImGui. + /// + public override void Draw() + { + if (ImGuiComponents.IconButton("forceReload", FontAwesomeIcon.Sync)) this.Load(); + if (ImGui.IsItemHovered()) ImGui.SetTooltip("Force Reload"); + ImGui.SameLine(); + var copy = ImGuiComponents.IconButton("copyAll", FontAwesomeIcon.ClipboardList); + if (ImGui.IsItemHovered()) ImGui.SetTooltip("Copy All"); + ImGui.SameLine(); + + ImGui.SetNextItemWidth(275.0f * ImGuiHelpers.GlobalScale); + if (ImGui.BeginCombo("Data Kind", this.dataKindNames[this.currentKind])) + { + foreach (var module in this.modules.OrderBy(module => this.dataKindNames[module.DataKind])) + { + if (ImGui.Selectable(this.dataKindNames[module.DataKind], this.currentKind == module.DataKind)) + { + this.currentKind = module.DataKind; + } + } + + ImGui.EndCombo(); + } + + ImGuiHelpers.ScaledDummy(10.0f); + + ImGui.BeginChild("scrolling", Vector2.Zero, false, ImGuiWindowFlags.HorizontalScrollbar); + + if (copy) + ImGui.LogToClipboard(); + + try + { + var selectedWidget = this.modules.FirstOrDefault(dataWindowWidget => dataWindowWidget.DataKind == this.currentKind); + + if (selectedWidget is { Ready: true }) + { + selectedWidget.Draw(); + } + else + { + ImGui.TextUnformatted("Data not ready."); + } + + this.isExcept = false; + } + catch (Exception ex) + { + if (!this.isExcept) + { + Log.Error(ex, "Could not draw data"); + } + + this.isExcept = true; + + ImGui.TextUnformatted(ex.ToString()); + } + + ImGui.EndChild(); + } + + private void Load() + { + foreach (var widget in this.modules) + { + widget.Load(); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs b/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs new file mode 100644 index 000000000..ebbdfff83 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Interface.Internal.Windows; + +/// +/// Class representing a date window entry. +/// +internal interface IDataWindowWidget +{ + /// + /// Gets the Data Kind for this data window module. + /// + DataKind DataKind { get; init; } + + /// + /// Gets or sets a value indicating whether this data window module is ready. + /// + bool Ready { get; protected set; } + + /// + /// Loads the necessary data for this data window module. + /// + void Load(); + + /// + /// Draws this data window module. + /// + void Draw(); +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs new file mode 100644 index 000000000..977037cc5 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs @@ -0,0 +1,32 @@ +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying addon inspector. +/// +internal class AddonInspectorWidget : IDataWindowWidget +{ + private UiDebug? addonInspector; + + /// + public DataKind DataKind { get; init; } = DataKind.Addon_Inspector; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.addonInspector = new UiDebug(); + + if (this.addonInspector is not null) + { + this.Ready = true; + } + } + + /// + public void Draw() + { + this.addonInspector?.Draw(); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs new file mode 100644 index 000000000..b26b7e311 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs @@ -0,0 +1,66 @@ +using Dalamud.Game.Gui; +using Dalamud.Memory; +using Dalamud.Utility; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying Addon Data. +/// +internal unsafe class AddonWidget : IDataWindowWidget +{ + private string inputAddonName = string.Empty; + private int inputAddonIndex; + private nint findAgentInterfacePtr; + + /// + public DataKind DataKind { get; init; } = DataKind.Addon; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var gameGui = Service.Get(); + + ImGui.InputText("Addon Name", ref this.inputAddonName, 256); + ImGui.InputInt("Addon Index", ref this.inputAddonIndex); + + if (this.inputAddonName.IsNullOrEmpty()) + return; + + var address = gameGui.GetAddonByName(this.inputAddonName, this.inputAddonIndex); + + if (address == nint.Zero) + { + ImGui.Text("Null"); + return; + } + + var addon = (FFXIVClientStructs.FFXIV.Component.GUI.AtkUnitBase*)address; + var name = MemoryHelper.ReadStringNullTerminated((nint)addon->Name); + ImGui.TextUnformatted($"{name} - 0x{address.ToInt64():X}\n v:{addon->IsVisible} x:{addon->X} y:{addon->Y} s:{addon->Scale}, w:{addon->RootNode->Width}, h:{addon->RootNode->Height}"); + + if (ImGui.Button("Find Agent")) + { + this.findAgentInterfacePtr = gameGui.FindAgentInterface(address); + } + + if (this.findAgentInterfacePtr != nint.Zero) + { + ImGui.TextUnformatted($"Agent: 0x{this.findAgentInterfacePtr.ToInt64():X}"); + ImGui.SameLine(); + + if (ImGui.Button("C")) + ImGui.SetClipboardText(this.findAgentInterfacePtr.ToInt64().ToString("X")); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs new file mode 100644 index 000000000..606fedadd --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; + +using Dalamud.Game; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget to display resolved .text sigs. +/// +internal class AddressesWidget : IDataWindowWidget +{ + private string inputSig = string.Empty; + private nint sigResult = nint.Zero; + + /// + public DataKind DataKind { get; init; } = DataKind.Address; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + ImGui.InputText(".text sig", ref this.inputSig, 400); + if (ImGui.Button("Resolve")) + { + try + { + var sigScanner = Service.Get(); + this.sigResult = sigScanner.ScanText(this.inputSig); + } + catch (KeyNotFoundException) + { + this.sigResult = new nint(-1); + } + } + + ImGui.Text($"Result: {this.sigResult.ToInt64():X}"); + ImGui.SameLine(); + if (ImGui.Button($"C##{this.sigResult.ToInt64():X}")) + ImGui.SetClipboardText(this.sigResult.ToInt64().ToString("X")); + + foreach (var debugScannedValue in BaseAddressResolver.DebugScannedValues) + { + ImGui.TextUnformatted($"{debugScannedValue.Key}"); + foreach (var valueTuple in debugScannedValue.Value) + { + ImGui.TextUnformatted( + $" {valueTuple.ClassName} - 0x{valueTuple.Address.ToInt64():X}"); + ImGui.SameLine(); + + if (ImGui.Button($"C##{valueTuple.Address.ToInt64():X}")) + ImGui.SetClipboardText(valueTuple.Address.ToInt64().ToString("X")); + } + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs new file mode 100644 index 000000000..cc4771847 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs @@ -0,0 +1,87 @@ +using Dalamud.Game.ClientState.Aetherytes; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying aetheryte table. +/// +internal class AetherytesWidget : IDataWindowWidget +{ + /// + public DataKind DataKind { get; init; } = DataKind.Aetherytes; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + if (!ImGui.BeginTable("##aetheryteTable", 11, ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders)) + return; + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("Idx", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("ID", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Zone", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Ward", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Plot", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Sub", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Gil", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Fav", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Shared", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableSetupColumn("Apartment", ImGuiTableColumnFlags.WidthFixed); + ImGui.TableHeadersRow(); + + var tpList = Service.Get(); + + for (var i = 0; i < tpList.Length; i++) + { + var info = tpList[i]; + if (info == null) + continue; + + ImGui.TableNextColumn(); // Idx + ImGui.TextUnformatted($"{i}"); + + ImGui.TableNextColumn(); // Name + ImGui.TextUnformatted($"{info.AetheryteData.GameData?.PlaceName.Value?.Name}"); + + ImGui.TableNextColumn(); // ID + ImGui.TextUnformatted($"{info.AetheryteId}"); + + ImGui.TableNextColumn(); // Zone + ImGui.TextUnformatted($"{info.TerritoryId}"); + + ImGui.TableNextColumn(); // Ward + ImGui.TextUnformatted($"{info.Ward}"); + + ImGui.TableNextColumn(); // Plot + ImGui.TextUnformatted($"{info.Plot}"); + + ImGui.TableNextColumn(); // Sub + ImGui.TextUnformatted($"{info.SubIndex}"); + + ImGui.TableNextColumn(); // Gil + ImGui.TextUnformatted($"{info.GilCost}"); + + ImGui.TableNextColumn(); // Favourite + ImGui.TextUnformatted($"{info.IsFavourite}"); + + ImGui.TableNextColumn(); // Shared + ImGui.TextUnformatted($"{info.IsSharedHouse}"); + + ImGui.TableNextColumn(); // Apartment + ImGui.TextUnformatted($"{info.IsAppartment}"); + } + + ImGui.EndTable(); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs new file mode 100644 index 000000000..df98f99a6 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs @@ -0,0 +1,173 @@ +using System; +using System.Numerics; + +using Dalamud.Memory; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying AtkArrayData. +/// +internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget +{ + /// + public DataKind DataKind { get; init; } = DataKind.AtkArrayData_Browser; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var fontWidth = ImGui.CalcTextSize("A").X; + var fontHeight = ImGui.GetTextLineHeightWithSpacing(); + var uiModule = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->GetUiModule(); + + if (uiModule == null) + { + ImGui.Text("UIModule unavailable."); + return; + } + + var atkArrayDataHolder = &uiModule->GetRaptureAtkModule()->AtkModule.AtkArrayDataHolder; + + if (ImGui.BeginTabBar("AtkArrayDataBrowserTabBar")) + { + if (ImGui.BeginTabItem($"NumberArrayData [{atkArrayDataHolder->NumberArrayCount}]")) + { + if (ImGui.BeginTable("NumberArrayDataTable", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY)) + { + ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed, fontWidth * 10); + ImGui.TableSetupColumn("Size", ImGuiTableColumnFlags.WidthFixed, fontWidth * 10); + ImGui.TableSetupColumn("Pointer", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableHeadersRow(); + for (var numberArrayIndex = 0; numberArrayIndex < atkArrayDataHolder->NumberArrayCount; numberArrayIndex++) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text($"{numberArrayIndex} [{numberArrayIndex * 8:X}]"); + ImGui.TableNextColumn(); + var numberArrayData = atkArrayDataHolder->NumberArrays[numberArrayIndex]; + if (numberArrayData != null) + { + ImGui.Text($"{numberArrayData->AtkArrayData.Size}"); + ImGui.TableNextColumn(); + if (ImGui.TreeNodeEx($"{(long)numberArrayData:X}###{numberArrayIndex}", ImGuiTreeNodeFlags.SpanFullWidth)) + { + ImGui.NewLine(); + var tableHeight = Math.Min(40, numberArrayData->AtkArrayData.Size + 4); + if (ImGui.BeginTable($"NumberArrayDataTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, new Vector2(0.0F, fontHeight * tableHeight))) + { + ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed, fontWidth * 6); + ImGui.TableSetupColumn("Hex", ImGuiTableColumnFlags.WidthFixed, fontWidth * 9); + ImGui.TableSetupColumn("Integer", ImGuiTableColumnFlags.WidthFixed, fontWidth * 12); + ImGui.TableSetupColumn("Float", ImGuiTableColumnFlags.WidthFixed, fontWidth * 20); + ImGui.TableHeadersRow(); + for (var numberIndex = 0; numberIndex < numberArrayData->AtkArrayData.Size; numberIndex++) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text($"{numberIndex}"); + ImGui.TableNextColumn(); + ImGui.Text($"{numberArrayData->IntArray[numberIndex]:X}"); + ImGui.TableNextColumn(); + ImGui.Text($"{numberArrayData->IntArray[numberIndex]}"); + ImGui.TableNextColumn(); + ImGui.Text($"{*(float*)&numberArrayData->IntArray[numberIndex]}"); + } + + ImGui.EndTable(); + } + + ImGui.TreePop(); + } + } + else + { + ImGui.TextDisabled("--"); + ImGui.TableNextColumn(); + ImGui.TextDisabled("--"); + } + } + + ImGui.EndTable(); + } + + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem($"StringArrayData [{atkArrayDataHolder->StringArrayCount}]")) + { + if (ImGui.BeginTable("StringArrayDataTable", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY)) + { + ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed, fontWidth * 10); + ImGui.TableSetupColumn("Size", ImGuiTableColumnFlags.WidthFixed, fontWidth * 10); + ImGui.TableSetupColumn("Pointer", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableHeadersRow(); + for (var stringArrayIndex = 0; stringArrayIndex < atkArrayDataHolder->StringArrayCount; stringArrayIndex++) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text($"{stringArrayIndex} [{stringArrayIndex * 8:X}]"); + ImGui.TableNextColumn(); + var stringArrayData = atkArrayDataHolder->StringArrays[stringArrayIndex]; + if (stringArrayData != null) + { + ImGui.Text($"{stringArrayData->AtkArrayData.Size}"); + ImGui.TableNextColumn(); + if (ImGui.TreeNodeEx($"{(long)stringArrayData:X}###{stringArrayIndex}", ImGuiTreeNodeFlags.SpanFullWidth)) + { + ImGui.NewLine(); + var tableHeight = Math.Min(40, stringArrayData->AtkArrayData.Size + 4); + if (ImGui.BeginTable($"StringArrayDataTable", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, new Vector2(0.0F, fontHeight * tableHeight))) + { + ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed, fontWidth * 6); + ImGui.TableSetupColumn("String", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableHeadersRow(); + for (var stringIndex = 0; stringIndex < stringArrayData->AtkArrayData.Size; stringIndex++) + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.Text($"{stringIndex}"); + ImGui.TableNextColumn(); + if (stringArrayData->StringArray[stringIndex] != null) + { + ImGui.Text($"{MemoryHelper.ReadSeStringNullTerminated(new IntPtr(stringArrayData->StringArray[stringIndex]))}"); + } + else + { + ImGui.TextDisabled("--"); + } + } + + ImGui.EndTable(); + } + + ImGui.TreePop(); + } + } + else + { + ImGui.TextDisabled("--"); + ImGui.TableNextColumn(); + ImGui.TextDisabled("--"); + } + } + + ImGui.EndTable(); + } + + ImGui.EndTabItem(); + } + + ImGui.EndTabBar(); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs new file mode 100644 index 000000000..2aeb9d10d --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs @@ -0,0 +1,110 @@ +using Dalamud.Game.ClientState.Buddy; +using Dalamud.Utility; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying data about the Buddy List. +/// +internal class BuddyListWidget : IDataWindowWidget +{ + private bool resolveGameData; + + /// + public DataKind DataKind { get; init; } = DataKind.Buddy_List; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var buddyList = Service.Get(); + + ImGui.Checkbox("Resolve GameData", ref this.resolveGameData); + + ImGui.Text($"BuddyList: {buddyList.BuddyListAddress.ToInt64():X}"); + { + var member = buddyList.CompanionBuddy; + if (member == null) + { + ImGui.Text("[Companion] null"); + } + else + { + ImGui.Text($"[Companion] {member.Address.ToInt64():X} - {member.ObjectId} - {member.DataID}"); + if (this.resolveGameData) + { + var gameObject = member.GameObject; + if (gameObject == null) + { + ImGui.Text("GameObject was null"); + } + else + { + Util.PrintGameObject(gameObject, "-", this.resolveGameData); + } + } + } + } + + { + var member = buddyList.PetBuddy; + if (member == null) + { + ImGui.Text("[Pet] null"); + } + else + { + ImGui.Text($"[Pet] {member.Address.ToInt64():X} - {member.ObjectId} - {member.DataID}"); + if (this.resolveGameData) + { + var gameObject = member.GameObject; + if (gameObject == null) + { + ImGui.Text("GameObject was null"); + } + else + { + Util.PrintGameObject(gameObject, "-", this.resolveGameData); + } + } + } + } + + { + var count = buddyList.Length; + if (count == 0) + { + ImGui.Text("[BattleBuddy] None present"); + } + else + { + for (var i = 0; i < count; i++) + { + var member = buddyList[i]; + ImGui.Text($"[BattleBuddy] [{i}] {member?.Address.ToInt64():X} - {member?.ObjectId} - {member?.DataID}"); + if (this.resolveGameData) + { + var gameObject = member?.GameObject; + if (gameObject == null) + { + ImGui.Text("GameObject was null"); + } + else + { + Util.PrintGameObject(gameObject, "-", this.resolveGameData); + } + } + } + } + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs new file mode 100644 index 000000000..e415431ba --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs @@ -0,0 +1,33 @@ +using Dalamud.Game.Command; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying command info. +/// +internal class CommandWidget : IDataWindowWidget +{ + /// + public DataKind DataKind { get; init; } = DataKind.Command; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var commandManager = Service.Get(); + + foreach (var command in commandManager.Commands) + { + ImGui.Text($"{command.Key}\n -> {command.Value.HelpMessage}\n -> In help: {command.Value.ShowInHelp}\n\n"); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs new file mode 100644 index 000000000..a5224589f --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs @@ -0,0 +1,52 @@ +using Dalamud.Game.ClientState.Conditions; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying current character condition flags. +/// +internal class ConditionWidget : IDataWindowWidget +{ + /// + public DataKind DataKind { get; init; } = DataKind.Condition; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var condition = Service.Get(); + +#if DEBUG + ImGui.Text($"ptr: 0x{condition.Address.ToInt64():X}"); +#endif + + ImGui.Text("Current Conditions:"); + ImGui.Separator(); + + var didAny = false; + + for (var i = 0; i < Condition.MaxConditionEntries; i++) + { + var typedCondition = (ConditionFlag)i; + var cond = condition[typedCondition]; + + if (!cond) continue; + + didAny = true; + + ImGui.Text($"ID: {i} Enum: {typedCondition}"); + } + + if (!didAny) + ImGui.Text("None. Talk to a shop NPC or visit a market board to find out more!"); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs new file mode 100644 index 000000000..3922f22b7 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs @@ -0,0 +1,29 @@ +using Dalamud.Configuration.Internal; +using Dalamud.Utility; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying configuration info. +/// +internal class ConfigurationWidget : IDataWindowWidget +{ + /// + public DataKind DataKind { get; init; } = DataKind.Configuration; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var config = Service.Get(); + Util.ShowObject(config); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs new file mode 100644 index 000000000..6ec741fe8 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -0,0 +1,53 @@ +using Dalamud.Plugin.Ipc.Internal; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying plugin data share modules. +/// +internal class DataShareWidget : IDataWindowWidget +{ + /// + public DataKind DataKind { get; init; } = DataKind.DataShare; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + if (!ImGui.BeginTable("###DataShareTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg)) + return; + + try + { + ImGui.TableSetupColumn("Shared Tag"); + ImGui.TableSetupColumn("Creator Assembly"); + ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Consumers"); + ImGui.TableHeadersRow(); + foreach (var share in Service.Get().GetAllShares()) + { + ImGui.TableNextColumn(); + ImGui.TextUnformatted(share.Tag); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(share.CreatorAssembly); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(share.Users.Length.ToString()); + ImGui.TableNextColumn(); + ImGui.TextUnformatted(string.Join(", ", share.Users)); + } + } + finally + { + ImGui.EndTable(); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs new file mode 100644 index 000000000..6d3a67e1a --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs @@ -0,0 +1,80 @@ +using Dalamud.Configuration.Internal; +using Dalamud.Game.Gui.Dtr; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying dtr test. +/// +internal class DtrBarWidget : IDataWindowWidget +{ + private DtrBarEntry? dtrTest1; + private DtrBarEntry? dtrTest2; + private DtrBarEntry? dtrTest3; + + /// + public DataKind DataKind { get; init; } = DataKind.Dtr_Bar; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + this.DrawDtrTestEntry(ref this.dtrTest1, "DTR Test #1"); + ImGui.Separator(); + this.DrawDtrTestEntry(ref this.dtrTest2, "DTR Test #2"); + ImGui.Separator(); + this.DrawDtrTestEntry(ref this.dtrTest3, "DTR Test #3"); + ImGui.Separator(); + + var configuration = Service.Get(); + if (configuration.DtrOrder != null) + { + ImGui.Separator(); + + foreach (var order in configuration.DtrOrder) + { + ImGui.Text(order); + } + } + } + + private void DrawDtrTestEntry(ref DtrBarEntry? entry, string title) + { + var dtrBar = Service.Get(); + + if (entry != null) + { + ImGui.Text(title); + + var text = entry.Text?.TextValue ?? string.Empty; + if (ImGui.InputText($"Text###{entry.Title}t", ref text, 255)) + entry.Text = text; + + var shown = entry.Shown; + if (ImGui.Checkbox($"Shown###{entry.Title}s", ref shown)) + entry.Shown = shown; + + if (ImGui.Button($"Remove###{entry.Title}r")) + { + entry.Remove(); + entry = null; + } + } + else + { + if (ImGui.Button($"Add###{title}")) + { + entry = dtrBar.Get(title, title); + } + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs new file mode 100644 index 000000000..78a93c1cc --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs @@ -0,0 +1,68 @@ +using Dalamud.Game.ClientState.Fates; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying the Fate Table. +/// +internal class FateTableWidget : IDataWindowWidget +{ + private bool resolveGameData; + + /// + public DataKind DataKind { get; init; } = DataKind.Fate_Table; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + ImGui.Checkbox("Resolve GameData", ref this.resolveGameData); + + var fateTable = Service.Get(); + + var stateString = string.Empty; + if (fateTable.Length == 0) + { + ImGui.TextUnformatted("No fates or data not ready."); + } + else + { + stateString += $"FateTableLen: {fateTable.Length}\n"; + + ImGui.TextUnformatted(stateString); + + for (var i = 0; i < fateTable.Length; i++) + { + var fate = fateTable[i]; + if (fate == null) + continue; + + var fateString = $"{fate.Address.ToInt64():X}:[{i}]" + + $" - Lv.{fate.Level} {fate.Name} ({fate.Progress}%)" + + $" - X{fate.Position.X} Y{fate.Position.Y} Z{fate.Position.Z}" + + $" - Territory {(this.resolveGameData ? (fate.TerritoryType.GameData?.Name ?? fate.TerritoryType.Id.ToString()) : fate.TerritoryType.Id.ToString())}\n"; + + fateString += $" StartTimeEpoch: {fate.StartTimeEpoch}" + + $" - Duration: {fate.Duration}" + + $" - State: {fate.State}" + + $" - GameData name: {(this.resolveGameData ? (fate.GameData.Name ?? fate.FateId.ToString()) : fate.FateId.ToString())}"; + + ImGui.TextUnformatted(fateString); + ImGui.SameLine(); + if (ImGui.Button("C")) + { + ImGui.SetClipboardText(fate.Address.ToString("X")); + } + } + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs new file mode 100644 index 000000000..99c1a3e02 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs @@ -0,0 +1,77 @@ +using System; +using System.Numerics; + +using Dalamud.Game.Gui.FlyText; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying fly text info. +/// +internal class FlyTextWidget : IDataWindowWidget +{ + private int flyActor; + private FlyTextKind flyKind; + private int flyVal1; + private int flyVal2; + private string flyText1 = string.Empty; + private string flyText2 = string.Empty; + private int flyIcon; + private int flyDmgIcon; + private Vector4 flyColor = new(1, 0, 0, 1); + + /// + public DataKind DataKind { get; init; } = DataKind.FlyText; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + if (ImGui.BeginCombo("Kind", this.flyKind.ToString())) + { + var names = Enum.GetNames(typeof(FlyTextKind)); + for (var i = 0; i < names.Length; i++) + { + if (ImGui.Selectable($"{names[i]} ({i})")) + this.flyKind = (FlyTextKind)i; + } + + ImGui.EndCombo(); + } + + ImGui.InputText("Text1", ref this.flyText1, 200); + ImGui.InputText("Text2", ref this.flyText2, 200); + + ImGui.InputInt("Val1", ref this.flyVal1); + ImGui.InputInt("Val2", ref this.flyVal2); + + ImGui.InputInt("Icon ID", ref this.flyIcon); + ImGui.InputInt("Damage Icon ID", ref this.flyDmgIcon); + ImGui.ColorEdit4("Color", ref this.flyColor); + ImGui.InputInt("Actor Index", ref this.flyActor); + var sendColor = ImGui.ColorConvertFloat4ToU32(this.flyColor); + + if (ImGui.Button("Send")) + { + Service.Get().AddFlyText( + this.flyKind, + unchecked((uint)this.flyActor), + unchecked((uint)this.flyVal1), + unchecked((uint)this.flyVal2), + this.flyText1, + this.flyText2, + sendColor, + unchecked((uint)this.flyIcon), + unchecked((uint)this.flyDmgIcon)); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs new file mode 100644 index 000000000..1ed5e9e83 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget to display FontAwesome Symbols. +/// +internal class FontAwesomeTestWidget : IDataWindowWidget +{ + private List? icons; + private List? iconNames; + private string[]? iconCategories; + private int selectedIconCategory; + private string iconSearchInput = string.Empty; + private bool iconSearchChanged = true; + + /// + public DataKind DataKind { get; init; } = DataKind.FontAwesome_Test; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); + + this.iconCategories ??= FontAwesomeHelpers.GetCategories(); + + if (this.iconSearchChanged) + { + this.icons = FontAwesomeHelpers.SearchIcons(this.iconSearchInput, this.iconCategories[this.selectedIconCategory]); + this.iconNames = this.icons.Select(icon => Enum.GetName(icon)!).ToList(); + this.iconSearchChanged = false; + } + + ImGui.SetNextItemWidth(160f); + var categoryIndex = this.selectedIconCategory; + if (ImGui.Combo("####FontAwesomeCategorySearch", ref categoryIndex, this.iconCategories, this.iconCategories.Length)) + { + this.selectedIconCategory = categoryIndex; + this.iconSearchChanged = true; + } + + ImGui.SameLine(170f); + ImGui.SetNextItemWidth(180f); + if (ImGui.InputTextWithHint($"###FontAwesomeInputSearch", "search icons", ref this.iconSearchInput, 50)) + { + this.iconSearchChanged = true; + } + + ImGuiHelpers.ScaledDummy(10f); + for (var i = 0; i < this.icons?.Count; i++) + { + ImGui.Text($"0x{(int)this.icons[i].ToIconChar():X}"); + ImGuiHelpers.ScaledRelativeSameLine(50f); + ImGui.Text($"{this.iconNames?[i]}"); + ImGuiHelpers.ScaledRelativeSameLine(280f); + ImGui.PushFont(UiBuilder.IconFont); + ImGui.Text(this.icons[i].ToIconString()); + ImGui.PopFont(); + ImGuiHelpers.ScaledDummy(2f); + } + + ImGui.PopStyleVar(); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs new file mode 100644 index 000000000..5c92e3ad1 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs @@ -0,0 +1,86 @@ +using System; + +using Dalamud.Game.ClientState.GamePad; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying gamepad info. +/// +internal class GamepadWidget : IDataWindowWidget +{ + /// + public DataKind DataKind { get; init; } = DataKind.Gamepad; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var gamepadState = Service.Get(); + + static void DrawHelper(string text, uint mask, Func resolve) + { + ImGui.Text($"{text} {mask:X4}"); + ImGui.Text($"DPadLeft {resolve(GamepadButtons.DpadLeft)} " + + $"DPadUp {resolve(GamepadButtons.DpadUp)} " + + $"DPadRight {resolve(GamepadButtons.DpadRight)} " + + $"DPadDown {resolve(GamepadButtons.DpadDown)} "); + ImGui.Text($"West {resolve(GamepadButtons.West)} " + + $"North {resolve(GamepadButtons.North)} " + + $"East {resolve(GamepadButtons.East)} " + + $"South {resolve(GamepadButtons.South)} "); + ImGui.Text($"L1 {resolve(GamepadButtons.L1)} " + + $"L2 {resolve(GamepadButtons.L2)} " + + $"R1 {resolve(GamepadButtons.R1)} " + + $"R2 {resolve(GamepadButtons.R2)} "); + ImGui.Text($"Select {resolve(GamepadButtons.Select)} " + + $"Start {resolve(GamepadButtons.Start)} " + + $"L3 {resolve(GamepadButtons.L3)} " + + $"R3 {resolve(GamepadButtons.R3)} "); + } + + ImGui.Text($"GamepadInput 0x{gamepadState.GamepadInputAddress.ToInt64():X}"); + +#if DEBUG + if (ImGui.IsItemHovered()) + ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); + + if (ImGui.IsItemClicked()) + ImGui.SetClipboardText($"0x{gamepadState.GamepadInputAddress.ToInt64():X}"); +#endif + + DrawHelper( + "Buttons Raw", + gamepadState.ButtonsRaw, + gamepadState.Raw); + DrawHelper( + "Buttons Pressed", + gamepadState.ButtonsPressed, + gamepadState.Pressed); + DrawHelper( + "Buttons Repeat", + gamepadState.ButtonsRepeat, + gamepadState.Repeat); + DrawHelper( + "Buttons Released", + gamepadState.ButtonsReleased, + gamepadState.Released); + ImGui.Text($"LeftStickLeft {gamepadState.LeftStickLeft:0.00} " + + $"LeftStickUp {gamepadState.LeftStickUp:0.00} " + + $"LeftStickRight {gamepadState.LeftStickRight:0.00} " + + $"LeftStickDown {gamepadState.LeftStickDown:0.00} "); + ImGui.Text($"RightStickLeft {gamepadState.RightStickLeft:0.00} " + + $"RightStickUp {gamepadState.RightStickUp:0.00} " + + $"RightStickRight {gamepadState.RightStickRight:0.00} " + + $"RightStickDown {gamepadState.RightStickDown:0.00} "); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs new file mode 100644 index 000000000..02862b33d --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs @@ -0,0 +1,72 @@ +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.JobGauge; +using Dalamud.Game.ClientState.JobGauge.Types; +using Dalamud.Utility; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying job gauge data. +/// +internal class GaugeWidget : IDataWindowWidget +{ + /// + public DataKind DataKind { get; init; } = DataKind.Gauge; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var clientState = Service.Get(); + var jobGauges = Service.Get(); + + var player = clientState.LocalPlayer; + if (player == null) + { + ImGui.Text("Player is not present"); + return; + } + + var jobID = player.ClassJob.Id; + JobGaugeBase? gauge = jobID switch + { + 19 => jobGauges.Get(), + 20 => jobGauges.Get(), + 21 => jobGauges.Get(), + 22 => jobGauges.Get(), + 23 => jobGauges.Get(), + 24 => jobGauges.Get(), + 25 => jobGauges.Get(), + 27 => jobGauges.Get(), + 28 => jobGauges.Get(), + 30 => jobGauges.Get(), + 31 => jobGauges.Get(), + 32 => jobGauges.Get(), + 33 => jobGauges.Get(), + 34 => jobGauges.Get(), + 35 => jobGauges.Get(), + 37 => jobGauges.Get(), + 38 => jobGauges.Get(), + 39 => jobGauges.Get(), + 40 => jobGauges.Get(), + _ => null, + }; + + if (gauge == null) + { + ImGui.Text("No supported gauge exists for this job."); + return; + } + + Util.ShowObject(gauge); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs new file mode 100644 index 000000000..aa565b1e6 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -0,0 +1,87 @@ +using System; +using System.Runtime.InteropServices; + +using Dalamud.Hooking; +using ImGuiNET; +using PInvoke; +using Serilog; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying hook information. +/// +internal class HookWidget : IDataWindowWidget +{ + private Hook? messageBoxMinHook; + private bool hookUseMinHook; + + private delegate int MessageBoxWDelegate( + IntPtr hWnd, + [MarshalAs(UnmanagedType.LPWStr)] string text, + [MarshalAs(UnmanagedType.LPWStr)] string caption, + NativeFunctions.MessageBoxType type); + + /// + public DataKind DataKind { get; init; } = DataKind.Hook; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + try + { + ImGui.Checkbox("Use MinHook", ref this.hookUseMinHook); + + if (ImGui.Button("Create")) + this.messageBoxMinHook = Hook.FromSymbol("User32", "MessageBoxW", this.MessageBoxWDetour, this.hookUseMinHook); + + if (ImGui.Button("Enable")) + this.messageBoxMinHook?.Enable(); + + if (ImGui.Button("Disable")) + this.messageBoxMinHook?.Disable(); + + if (ImGui.Button("Call Original")) + this.messageBoxMinHook?.Original(IntPtr.Zero, "Hello from .Original", "Hook Test", NativeFunctions.MessageBoxType.Ok); + + if (ImGui.Button("Dispose")) + { + this.messageBoxMinHook?.Dispose(); + this.messageBoxMinHook = null; + } + + if (ImGui.Button("Test")) + _ = NativeFunctions.MessageBoxW(IntPtr.Zero, "Hi", "Hello", NativeFunctions.MessageBoxType.Ok); + + if (this.messageBoxMinHook != null) + ImGui.Text("Enabled: " + this.messageBoxMinHook?.IsEnabled); + } + catch (Exception ex) + { + Log.Error(ex, "MinHook error caught"); + } + } + + private int MessageBoxWDetour(IntPtr hwnd, string text, string caption, NativeFunctions.MessageBoxType type) + { + Log.Information("[DATAHOOK] {Hwnd} {Text} {Caption} {Type}", hwnd, text, caption, type); + + var result = this.messageBoxMinHook!.Original(hwnd, "Cause Access Violation?", caption, NativeFunctions.MessageBoxType.YesNo); + + if (result == (int)User32.MessageBoxResult.IDYES) + { + Marshal.ReadByte(IntPtr.Zero); + } + + return result; + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs new file mode 100644 index 000000000..8afce718f --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -0,0 +1,74 @@ +using System; + +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Windowing; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying ImGui test. +/// +internal class ImGuiWidget : IDataWindowWidget +{ + /// + public DataKind DataKind { get; init; } = DataKind.ImGui; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var interfaceManager = Service.Get(); + var notifications = Service.Get(); + + ImGui.Text("Monitor count: " + ImGui.GetPlatformIO().Monitors.Size); + ImGui.Text("OverrideGameCursor: " + interfaceManager.OverrideGameCursor); + + ImGui.Button("THIS IS A BUTTON###hoverTestButton"); + interfaceManager.OverrideGameCursor = !ImGui.IsItemHovered(); + + ImGui.Separator(); + + ImGui.TextUnformatted($"WindowSystem.TimeSinceLastAnyFocus: {WindowSystem.TimeSinceLastAnyFocus.TotalMilliseconds:0}ms"); + + ImGui.Separator(); + + if (ImGui.Button("Add random notification")) + { + var rand = new Random(); + + var title = rand.Next(0, 5) switch + { + 0 => "This is a toast", + 1 => "Truly, a toast", + 2 => "I am testing this toast", + 3 => "I hope this looks right", + 4 => "Good stuff", + 5 => "Nice", + _ => null, + }; + + var type = rand.Next(0, 4) switch + { + 0 => NotificationType.Error, + 1 => NotificationType.Warning, + 2 => NotificationType.Info, + 3 => NotificationType.Success, + 4 => NotificationType.None, + _ => NotificationType.None, + }; + + const string text = "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; + + notifications.AddNotification(text, title, type); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs new file mode 100644 index 000000000..accc48b4b --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs @@ -0,0 +1,50 @@ +using Dalamud.Game.ClientState.Keys; +using Dalamud.Interface.Colors; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying keyboard state. +/// +internal class KeyStateWidget : IDataWindowWidget +{ + /// + public DataKind DataKind { get; init; } = DataKind.KeyState; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var keyState = Service.Get(); + + ImGui.Columns(4); + + var i = 0; + foreach (var vkCode in keyState.GetValidVirtualKeys()) + { + var code = (int)vkCode; + var value = keyState[code]; + + ImGui.PushStyleColor(ImGuiCol.Text, value ? ImGuiColors.HealerGreen : ImGuiColors.DPSRed); + + ImGui.Text($"{vkCode} ({code})"); + + ImGui.PopStyleColor(); + + i++; + if (i % 24 == 0) + ImGui.NextColumn(); + } + + ImGui.Columns(1); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs new file mode 100644 index 000000000..dd41315f2 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs @@ -0,0 +1,119 @@ +using System; +using System.Numerics; + +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.Gui; +using Dalamud.Utility; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget to display the Object Table. +/// +internal class ObjectTableWidget : IDataWindowWidget +{ + private bool resolveGameData; + private bool drawCharacters; + private float maxCharaDrawDistance = 20.0f; + + /// + public DataKind DataKind { get; init; } = DataKind.Object_Table; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + ImGui.Checkbox("Resolve GameData", ref this.resolveGameData); + + var chatGui = Service.Get(); + var clientState = Service.Get(); + var gameGui = Service.Get(); + var objectTable = Service.Get(); + + var stateString = string.Empty; + + if (clientState.LocalPlayer == null) + { + ImGui.TextUnformatted("LocalPlayer null."); + } + else if (clientState.IsPvPExcludingDen) + { + ImGui.TextUnformatted("Cannot access object table while in PvP."); + } + else + { + stateString += $"ObjectTableLen: {objectTable.Length}\n"; + stateString += $"LocalPlayerName: {clientState.LocalPlayer.Name}\n"; + stateString += $"CurrentWorldName: {(this.resolveGameData ? clientState.LocalPlayer.CurrentWorld.GameData?.Name : clientState.LocalPlayer.CurrentWorld.Id.ToString())}\n"; + stateString += $"HomeWorldName: {(this.resolveGameData ? clientState.LocalPlayer.HomeWorld.GameData?.Name : clientState.LocalPlayer.HomeWorld.Id.ToString())}\n"; + stateString += $"LocalCID: {clientState.LocalContentId:X}\n"; + stateString += $"LastLinkedItem: {chatGui.LastLinkedItemId}\n"; + stateString += $"TerritoryType: {clientState.TerritoryType}\n\n"; + + ImGui.TextUnformatted(stateString); + + ImGui.Checkbox("Draw characters on screen", ref this.drawCharacters); + ImGui.SliderFloat("Draw Distance", ref this.maxCharaDrawDistance, 2f, 40f); + + for (var i = 0; i < objectTable.Length; i++) + { + var obj = objectTable[i]; + + if (obj == null) + continue; + + Util.PrintGameObject(obj, i.ToString(), this.resolveGameData); + + if (this.drawCharacters && gameGui.WorldToScreen(obj.Position, out var screenCoords)) + { + // So, while WorldToScreen will return false if the point is off of game client screen, to + // to avoid performance issues, we have to manually determine if creating a window would + // produce a new viewport, and skip rendering it if so + var objectText = $"{obj.Address.ToInt64():X}:{obj.ObjectId:X}[{i}] - {obj.ObjectKind} - {obj.Name}"; + + var screenPos = ImGui.GetMainViewport().Pos; + var screenSize = ImGui.GetMainViewport().Size; + + var windowSize = ImGui.CalcTextSize(objectText); + + // Add some extra safety padding + windowSize.X += ImGui.GetStyle().WindowPadding.X + 10; + windowSize.Y += ImGui.GetStyle().WindowPadding.Y + 10; + + if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X || + screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y) + continue; + + if (obj.YalmDistanceX > this.maxCharaDrawDistance) + continue; + + ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y)); + + ImGui.SetNextWindowBgAlpha(Math.Max(1f - (obj.YalmDistanceX / this.maxCharaDrawDistance), 0.2f)); + if (ImGui.Begin( + $"Actor{i}##ActorWindow{i}", + ImGuiWindowFlags.NoDecoration | + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoSavedSettings | + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoMouseInputs | + ImGuiWindowFlags.NoDocking | + ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoNav)) + ImGui.Text(objectText); + ImGui.End(); + } + } + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs new file mode 100644 index 000000000..c5ac1fb8f --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs @@ -0,0 +1,63 @@ +using Dalamud.Game.ClientState.Party; +using Dalamud.Utility; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying information about the current party. +/// +internal class PartyListWidget : IDataWindowWidget +{ + private bool resolveGameData; + + /// + public DataKind DataKind { get; init; } = DataKind.Party_List; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var partyList = Service.Get(); + + ImGui.Checkbox("Resolve GameData", ref this.resolveGameData); + + ImGui.Text($"GroupManager: {partyList.GroupManagerAddress.ToInt64():X}"); + ImGui.Text($"GroupList: {partyList.GroupListAddress.ToInt64():X}"); + ImGui.Text($"AllianceList: {partyList.AllianceListAddress.ToInt64():X}"); + + ImGui.Text($"{partyList.Length} Members"); + + for (var i = 0; i < partyList.Length; i++) + { + var member = partyList[i]; + if (member == null) + { + ImGui.Text($"[{i}] was null"); + continue; + } + + ImGui.Text($"[{i}] {member.Address.ToInt64():X} - {member.Name} - {member.GameObject?.ObjectId}"); + if (this.resolveGameData) + { + var actor = member.GameObject; + if (actor == null) + { + ImGui.Text("Actor was null"); + } + else + { + Util.PrintGameObject(actor, "-", this.resolveGameData); + } + } + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs new file mode 100644 index 000000000..9aae9bba3 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs @@ -0,0 +1,84 @@ +using System; + +using Dalamud.Plugin.Ipc; +using Dalamud.Plugin.Ipc.Internal; +using Dalamud.Utility; +using ImGuiNET; +using Serilog; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for testing plugin IPC systems. +/// +internal class PluginIpcWidget : IDataWindowWidget +{ + // IPC + private ICallGateProvider? ipcPub; + private ICallGateSubscriber? ipcSub; + private string callGateResponse = string.Empty; + + /// + public DataKind DataKind { get; init; } = DataKind.Plugin_IPC; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + if (this.ipcPub == null) + { + this.ipcPub = new CallGatePubSub("dataDemo1"); + + this.ipcPub.RegisterAction(msg => + { + Log.Information("Data action was called: {Msg}", msg); + }); + + this.ipcPub.RegisterFunc(msg => + { + Log.Information("Data func was called: {Msg}", msg); + return Guid.NewGuid().ToString(); + }); + } + + if (this.ipcSub == null) + { + this.ipcSub = new CallGatePubSub("dataDemo1"); + this.ipcSub.Subscribe(_ => + { + Log.Information("PONG1"); + }); + this.ipcSub.Subscribe(_ => + { + Log.Information("PONG2"); + }); + this.ipcSub.Subscribe(_ => throw new Exception("PONG3")); + } + + if (ImGui.Button("PING")) + { + this.ipcPub.SendMessage("PING"); + } + + if (ImGui.Button("Action")) + { + this.ipcSub.InvokeAction("button1"); + } + + if (ImGui.Button("Func")) + { + this.callGateResponse = this.ipcSub.InvokeFunc("button2"); + } + + if (!this.callGateResponse.IsNullOrEmpty()) + ImGui.Text($"Response: {this.callGateResponse}"); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs new file mode 100644 index 000000000..a642c439d --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs @@ -0,0 +1,33 @@ +using Dalamud.Game.Text; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying test data for SE Font Symbols. +/// +internal class SeFontTestWidget : IDataWindowWidget +{ + /// + public DataKind DataKind { get; init; } = DataKind.SE_Font_Test; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var specialChars = string.Empty; + + for (var i = 0xE020; i <= 0xE0DB; i++) + specialChars += $"0x{i:X} - {(SeIconChar)i} - {(char)i}\n"; + + ImGui.TextUnformatted(specialChars); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs new file mode 100644 index 000000000..f414e0957 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs @@ -0,0 +1,37 @@ +using Dalamud.Data; +using ImGuiNET; +using Newtonsoft.Json; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget to display the currently set server opcodes. +/// +internal class ServerOpcodeWidget : IDataWindowWidget +{ + private string? serverOpString; + + /// + public DataKind DataKind { get; init; } = DataKind.Server_OpCode; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + var dataManager = Service.Get(); + + if (dataManager.IsDataReady) + { + this.serverOpString = JsonConvert.SerializeObject(dataManager.ServerOpCodes, Formatting.Indented); + this.Ready = true; + } + } + + /// + public void Draw() + { + ImGui.TextUnformatted(this.serverOpString ?? "serverOpString not initialized"); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs new file mode 100644 index 000000000..656efe388 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs @@ -0,0 +1,30 @@ +using ImGuiNET; +using Newtonsoft.Json; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying start info. +/// +internal class StartInfoWidget : IDataWindowWidget +{ + /// + public DataKind DataKind { get; init; } = DataKind.StartInfo; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var startInfo = Service.Get(); + + ImGui.Text(JsonConvert.SerializeObject(startInfo, Formatting.Indented)); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs new file mode 100644 index 000000000..07d6e8f72 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs @@ -0,0 +1,88 @@ +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Objects; +using Dalamud.Utility; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying target info. +/// +internal class TargetWidget : IDataWindowWidget +{ + private bool resolveGameData; + + /// + public DataKind DataKind { get; init; } = DataKind.Target; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + ImGui.Checkbox("Resolve GameData", ref this.resolveGameData); + + var clientState = Service.Get(); + var targetMgr = Service.Get(); + + if (targetMgr.Target != null) + { + Util.PrintGameObject(targetMgr.Target, "CurrentTarget", this.resolveGameData); + + ImGui.Text("Target"); + Util.ShowGameObjectStruct(targetMgr.Target); + + var tot = targetMgr.Target.TargetObject; + if (tot != null) + { + ImGuiHelpers.ScaledDummy(10); + + ImGui.Separator(); + ImGui.Text("ToT"); + Util.ShowGameObjectStruct(tot); + } + + ImGuiHelpers.ScaledDummy(10); + } + + if (targetMgr.FocusTarget != null) + Util.PrintGameObject(targetMgr.FocusTarget, "FocusTarget", this.resolveGameData); + + if (targetMgr.MouseOverTarget != null) + Util.PrintGameObject(targetMgr.MouseOverTarget, "MouseOverTarget", this.resolveGameData); + + if (targetMgr.PreviousTarget != null) + Util.PrintGameObject(targetMgr.PreviousTarget, "PreviousTarget", this.resolveGameData); + + if (targetMgr.SoftTarget != null) + Util.PrintGameObject(targetMgr.SoftTarget, "SoftTarget", this.resolveGameData); + + if (ImGui.Button("Clear CT")) + targetMgr.ClearTarget(); + + if (ImGui.Button("Clear FT")) + targetMgr.ClearFocusTarget(); + + var localPlayer = clientState.LocalPlayer; + + if (localPlayer != null) + { + if (ImGui.Button("Set CT")) + targetMgr.SetTarget(localPlayer); + + if (ImGui.Button("Set FT")) + targetMgr.SetFocusTarget(localPlayer); + } + else + { + ImGui.Text("LocalPlayer is null."); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs new file mode 100644 index 000000000..7d91cd154 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs @@ -0,0 +1,256 @@ +// ReSharper disable MethodSupportsCancellation // Using alternative method of cancelling tasks by throwing exceptions. +using System; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Game; +using Dalamud.Interface.Colors; +using Dalamud.Logging.Internal; +using ImGuiNET; +using Serilog; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying task scheduler test. +/// +internal class TaskSchedulerWidget : IDataWindowWidget +{ + private CancellationTokenSource taskSchedulerCancelSource = new(); + + /// + public DataKind DataKind { get; init; } = DataKind.TaskSched; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + if (ImGui.Button("Clear list")) + { + TaskTracker.Clear(); + } + + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(10); + ImGui.SameLine(); + + if (ImGui.Button("Cancel using CancellationTokenSource")) + { + this.taskSchedulerCancelSource.Cancel(); + this.taskSchedulerCancelSource = new(); + } + + ImGui.Text("Run in any thread: "); + ImGui.SameLine(); + + if (ImGui.Button("Short Task.Run")) + { + Task.Run(() => { Thread.Sleep(500); }); + } + + ImGui.SameLine(); + + if (ImGui.Button("Task in task(Delay)")) + { + var token = this.taskSchedulerCancelSource.Token; + Task.Run(async () => await this.TestTaskInTaskDelay(token), token); + } + + ImGui.SameLine(); + + if (ImGui.Button("Task in task(Sleep)")) + { + Task.Run(async () => await this.TestTaskInTaskSleep()); + } + + ImGui.SameLine(); + + if (ImGui.Button("Faulting task")) + { + Task.Run(() => + { + Thread.Sleep(200); + + string a = null; + a.Contains("dalamud"); // Intentional null exception. + }); + } + + ImGui.Text("Run in Framework.Update: "); + ImGui.SameLine(); + + if (ImGui.Button("ASAP")) + { + Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token)); + } + + ImGui.SameLine(); + + if (ImGui.Button("In 1s")) + { + Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1))); + } + + ImGui.SameLine(); + + if (ImGui.Button("In 60f")) + { + Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token, delayTicks: 60)); + } + + ImGui.SameLine(); + + if (ImGui.Button("Error in 1s")) + { + Task.Run(async () => await Service.Get().RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1))); + } + + ImGui.SameLine(); + + if (ImGui.Button("As long as it's in Framework Thread")) + { + Task.Run(async () => await Service.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); })); + Service.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait(); + } + + if (ImGui.Button("Drown in tasks")) + { + var token = this.taskSchedulerCancelSource.Token; + Task.Run( + () => + { + for (var i = 0; i < 100; i++) + { + token.ThrowIfCancellationRequested(); + Task.Run( + () => + { + for (var j = 0; j < 100; j++) + { + token.ThrowIfCancellationRequested(); + Task.Run( + () => + { + for (var k = 0; k < 100; k++) + { + token.ThrowIfCancellationRequested(); + Task.Run( + () => + { + for (var l = 0; l < 100; l++) + { + token.ThrowIfCancellationRequested(); + Task.Run( + async () => + { + for (var m = 0; m < 100; m++) + { + token.ThrowIfCancellationRequested(); + await Task.Delay(1, token); + } + }); + } + }); + } + }); + } + }); + } + }); + } + + ImGui.SameLine(); + + ImGuiHelpers.ScaledDummy(20); + + // Needed to init the task tracker, if we're not on a debug build + Service.Get().Enable(); + + for (var i = 0; i < TaskTracker.Tasks.Count; i++) + { + var task = TaskTracker.Tasks[i]; + var subTime = DateTime.Now; + if (task.Task == null) + subTime = task.FinishTime; + + switch (task.Status) + { + case TaskStatus.Created: + case TaskStatus.WaitingForActivation: + case TaskStatus.WaitingToRun: + ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.DalamudGrey); + break; + case TaskStatus.Running: + case TaskStatus.WaitingForChildrenToComplete: + ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedBlue); + break; + case TaskStatus.RanToCompletion: + ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedGreen); + break; + case TaskStatus.Canceled: + case TaskStatus.Faulted: + ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.DalamudRed); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + if (ImGui.CollapsingHeader($"#{task.Id} - {task.Status} {(subTime - task.StartTime).TotalMilliseconds}ms###task{i}")) + { + task.IsBeingViewed = true; + + if (ImGui.Button("CANCEL (May not work)")) + { + try + { + var cancelFunc = + typeof(Task).GetMethod("InternalCancel", BindingFlags.NonPublic | BindingFlags.Instance); + cancelFunc?.Invoke(task, null); + } + catch (Exception ex) + { + Log.Error(ex, "Could not cancel task"); + } + } + + ImGuiHelpers.ScaledDummy(10); + + ImGui.TextUnformatted(task.StackTrace?.ToString()); + + if (task.Exception != null) + { + ImGuiHelpers.ScaledDummy(15); + ImGui.TextColored(ImGuiColors.DalamudRed, "EXCEPTION:"); + ImGui.TextUnformatted(task.Exception.ToString()); + } + } + else + { + task.IsBeingViewed = false; + } + + ImGui.PopStyleColor(1); + } + } + + private async Task TestTaskInTaskDelay(CancellationToken token) + { + await Task.Delay(5000, token); + } + +#pragma warning disable 1998 + private async Task TestTaskInTaskSleep() +#pragma warning restore 1998 + { + Thread.Sleep(5000); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs new file mode 100644 index 000000000..4a0cfe21a --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -0,0 +1,69 @@ +using System; +using System.Numerics; + +using Dalamud.Data; +using Dalamud.Utility; +using ImGuiNET; +using ImGuiScene; +using Serilog; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying texture test. +/// +internal class TexWidget : IDataWindowWidget +{ + private string inputTexPath = string.Empty; + private TextureWrap? debugTex; + private Vector2 inputTexUv0 = Vector2.Zero; + private Vector2 inputTexUv1 = Vector2.One; + private Vector4 inputTintCol = Vector4.One; + private Vector2 inputTexScale = Vector2.Zero; + + /// + public DataKind DataKind { get; init; } = DataKind.Tex; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var dataManager = Service.Get(); + + ImGui.InputText("Tex Path", ref this.inputTexPath, 255); + ImGui.InputFloat2("UV0", ref this.inputTexUv0); + ImGui.InputFloat2("UV1", ref this.inputTexUv1); + ImGui.InputFloat4("Tint", ref this.inputTintCol); + ImGui.InputFloat2("Scale", ref this.inputTexScale); + + if (ImGui.Button("Load Tex")) + { + try + { + this.debugTex = dataManager.GetImGuiTexture(this.inputTexPath); + this.inputTexScale = new Vector2(this.debugTex?.Width ?? 0, this.debugTex?.Height ?? 0); + } + catch (Exception ex) + { + Log.Error(ex, "Could not load tex"); + } + } + + ImGuiHelpers.ScaledDummy(10); + + if (this.debugTex != null) + { + ImGui.Image(this.debugTex.ImGuiHandle, this.inputTexScale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol); + ImGuiHelpers.ScaledDummy(5); + Util.ShowObject(this.debugTex); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs new file mode 100644 index 000000000..c75230e73 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs @@ -0,0 +1,74 @@ +using System.Numerics; + +using Dalamud.Game.Gui.Toast; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying toast test. +/// +internal class ToastWidget : IDataWindowWidget +{ + private string inputTextToast = string.Empty; + private int toastPosition; + private int toastSpeed; + private int questToastPosition; + private bool questToastSound; + private int questToastIconId; + private bool questToastCheckmark; + + /// + public DataKind DataKind { get; init; } = DataKind.Toast; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var toastGui = Service.Get(); + + ImGui.InputText("Toast text", ref this.inputTextToast, 200); + + ImGui.Combo("Toast Position", ref this.toastPosition, new[] { "Bottom", "Top", }, 2); + ImGui.Combo("Toast Speed", ref this.toastSpeed, new[] { "Slow", "Fast", }, 2); + ImGui.Combo("Quest Toast Position", ref this.questToastPosition, new[] { "Centre", "Right", "Left" }, 3); + ImGui.Checkbox("Quest Checkmark", ref this.questToastCheckmark); + ImGui.Checkbox("Quest Play Sound", ref this.questToastSound); + ImGui.InputInt("Quest Icon ID", ref this.questToastIconId); + + ImGuiHelpers.ScaledDummy(new Vector2(10, 10)); + + if (ImGui.Button("Show toast")) + { + toastGui.ShowNormal(this.inputTextToast, new ToastOptions + { + Position = (ToastPosition)this.toastPosition, + Speed = (ToastSpeed)this.toastSpeed, + }); + } + + if (ImGui.Button("Show Quest toast")) + { + toastGui.ShowQuest(this.inputTextToast, new QuestToastOptions + { + Position = (QuestToastPosition)this.questToastPosition, + DisplayCheckmark = this.questToastCheckmark, + IconId = (uint)this.questToastIconId, + PlaySound = this.questToastSound, + }); + } + + if (ImGui.Button("Show Error toast")) + { + toastGui.ShowError(this.inputTextToast); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs new file mode 100644 index 000000000..1d0ccdce6 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -0,0 +1,60 @@ +using System.Numerics; + +using Dalamud.Data; +using ImGuiNET; +using Lumina.Excel.GeneratedSheets; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Widget for displaying all UI Colors from Lumina. +/// +internal class UIColorWidget : IDataWindowWidget +{ + /// + public DataKind DataKind { get; init; } = DataKind.UIColor; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var colorSheet = Service.Get().GetExcelSheet(); + if (colorSheet is null) return; + + foreach (var color in colorSheet) + { + this.DrawUiColor(color); + } + } + + private void DrawUiColor(UIColor color) + { + ImGui.Text($"[{color.RowId:D3}] "); + ImGui.SameLine(); + ImGui.TextColored(this.ConvertToVector4(color.Unknown2), $"Unknown2 "); + ImGui.SameLine(); + ImGui.TextColored(this.ConvertToVector4(color.UIForeground), "UIForeground "); + ImGui.SameLine(); + ImGui.TextColored(this.ConvertToVector4(color.Unknown3), "Unknown3 "); + ImGui.SameLine(); + ImGui.TextColored(this.ConvertToVector4(color.UIGlow), "UIGlow"); + } + + private Vector4 ConvertToVector4(uint color) + { + var r = (byte)(color >> 24); + var g = (byte)(color >> 16); + var b = (byte)(color >> 8); + var a = (byte)color; + + return new Vector4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f); + } +} diff --git a/Dalamud/Interface/Internal/Windows/DataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs deleted file mode 100644 index c46916343..000000000 --- a/Dalamud/Interface/Internal/Windows/DataWindow.cs +++ /dev/null @@ -1,1886 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -using Dalamud.Configuration.Internal; -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Game.ClientState; -using Dalamud.Game.ClientState.Aetherytes; -using Dalamud.Game.ClientState.Buddy; -using Dalamud.Game.ClientState.Conditions; -using Dalamud.Game.ClientState.Fates; -using Dalamud.Game.ClientState.GamePad; -using Dalamud.Game.ClientState.JobGauge; -using Dalamud.Game.ClientState.JobGauge.Types; -using Dalamud.Game.ClientState.Keys; -using Dalamud.Game.ClientState.Objects; -using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Game.ClientState.Party; -using Dalamud.Game.Command; -using Dalamud.Game.Gui; -using Dalamud.Game.Gui.Dtr; -using Dalamud.Game.Gui.FlyText; -using Dalamud.Game.Gui.Toast; -using Dalamud.Game.Text; -using Dalamud.Hooking; -using Dalamud.Interface.Colors; -using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Windowing; -using Dalamud.Logging.Internal; -using Dalamud.Memory; -using Dalamud.Plugin.Ipc; -using Dalamud.Plugin.Ipc.Internal; -using Dalamud.Utility; -using ImGuiNET; -using ImGuiScene; -using Lumina.Excel.GeneratedSheets; -using Newtonsoft.Json; -using PInvoke; -using Serilog; - -using Condition = Dalamud.Game.ClientState.Conditions.Condition; - -namespace Dalamud.Interface.Internal.Windows; - -/// -/// Class responsible for drawing the data/debug window. -/// -internal class DataWindow : Window -{ - private readonly string[] dataKindNames = Enum.GetNames(typeof(DataKind)).Select(k => k.Replace("_", " ")).ToArray(); - - private bool wasReady; - private bool isExcept; - private string serverOpString; - private DataKind currentKind; - - private bool drawCharas = false; - private float maxCharaDrawDistance = 20; - - private string inputSig = string.Empty; - private IntPtr sigResult = IntPtr.Zero; - - private string inputAddonName = string.Empty; - private int inputAddonIndex; - - private IntPtr findAgentInterfacePtr; - - private bool resolveGameData = false; - private bool resolveObjects = false; - - private UiDebug addonInspector = null; - - private Hook? messageBoxMinHook; - private bool hookUseMinHook = false; - - // FontAwesome - private List? icons; - private List iconNames; - private string[]? iconCategories; - private int selectedIconCategory; - private string iconSearchInput = string.Empty; - private bool iconSearchChanged = true; - - // IPC - private ICallGateProvider ipcPub; - private ICallGateSubscriber ipcSub; - private string callGateResponse = string.Empty; - - // Toast fields - private string inputTextToast = string.Empty; - private int toastPosition = 0; - private int toastSpeed = 0; - private int questToastPosition = 0; - private bool questToastSound = false; - private int questToastIconId = 0; - private bool questToastCheckmark = false; - - // Fly text fields - private int flyActor; - private FlyTextKind flyKind; - private int flyVal1; - private int flyVal2; - private string flyText1 = string.Empty; - private string flyText2 = string.Empty; - private int flyIcon; - private int flyDmgIcon; - private Vector4 flyColor = new(1, 0, 0, 1); - - // ImGui fields - private string inputTexPath = string.Empty; - private TextureWrap debugTex = null; - private Vector2 inputTexUv0 = Vector2.Zero; - private Vector2 inputTexUv1 = Vector2.One; - private Vector4 inputTintCol = Vector4.One; - private Vector2 inputTexScale = Vector2.Zero; - - // DTR - private DtrBarEntry? dtrTest1; - private DtrBarEntry? dtrTest2; - private DtrBarEntry? dtrTest3; - - // Task Scheduler - private CancellationTokenSource taskSchedCancelSource = new(); - - private uint copyButtonIndex = 0; - - /// - /// Initializes a new instance of the class. - /// - public DataWindow() - : base("Dalamud Data") - { - this.Size = new Vector2(500, 500); - this.SizeCondition = ImGuiCond.FirstUseEver; - - this.RespectCloseHotkey = false; - - this.Load(); - } - - private delegate int MessageBoxWDelegate( - IntPtr hWnd, - [MarshalAs(UnmanagedType.LPWStr)] string text, - [MarshalAs(UnmanagedType.LPWStr)] string caption, - NativeFunctions.MessageBoxType type); - - private enum DataKind - { - Server_OpCode, - Address, - Object_Table, - Fate_Table, - SE_Font_Test, - FontAwesome_Test, - Party_List, - Buddy_List, - Plugin_IPC, - Condition, - Gauge, - Command, - Addon, - Addon_Inspector, - AtkArrayData_Browser, - StartInfo, - Target, - Toast, - FlyText, - ImGui, - Tex, - KeyState, - Gamepad, - Configuration, - TaskSched, - Hook, - Aetherytes, - Dtr_Bar, - UIColor, - DataShare, - } - - /// - public override void OnOpen() - { - } - - /// - public override void OnClose() - { - } - - /// - /// Set the DataKind dropdown menu. - /// - /// Data kind name, can be lower and/or without spaces. - public void SetDataKind(string dataKind) - { - if (string.IsNullOrEmpty(dataKind)) - return; - - dataKind = dataKind switch - { - "ai" => "Addon Inspector", - "at" => "Object Table", // Actor Table - "ot" => "Object Table", - "uic" => "UIColor", - _ => dataKind, - }; - - dataKind = dataKind.Replace(" ", string.Empty).ToLower(); - - var matched = Enum.GetValues() - .Where(kind => Enum.GetName(kind).Replace("_", string.Empty).ToLower() == dataKind) - .FirstOrDefault(); - - if (matched != default) - { - this.currentKind = matched; - } - else - { - Service.Get().PrintError($"/xldata: Invalid data type {dataKind}"); - } - } - - /// - /// Draw the window via ImGui. - /// - public override void Draw() - { - this.copyButtonIndex = 0; - - // Main window - if (ImGui.Button("Force Reload")) - this.Load(); - ImGui.SameLine(); - var copy = ImGui.Button("Copy all"); - ImGui.SameLine(); - - var currentKindIndex = (int)this.currentKind; - if (ImGui.Combo("Data kind", ref currentKindIndex, this.dataKindNames, this.dataKindNames.Length)) - { - this.currentKind = (DataKind)currentKindIndex; - } - - ImGui.Checkbox("Resolve GameData", ref this.resolveGameData); - - ImGui.BeginChild("scrolling", new Vector2(0, 0), false, ImGuiWindowFlags.HorizontalScrollbar); - - if (copy) - ImGui.LogToClipboard(); - - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0)); - - try - { - if (this.wasReady) - { - switch (this.currentKind) - { - case DataKind.Server_OpCode: - this.DrawServerOpCode(); - break; - - case DataKind.Address: - this.DrawAddress(); - break; - - case DataKind.Object_Table: - this.DrawObjectTable(); - break; - - case DataKind.Fate_Table: - this.DrawFateTable(); - break; - - case DataKind.SE_Font_Test: - this.DrawSEFontTest(); - break; - - case DataKind.FontAwesome_Test: - this.DrawFontAwesomeTest(); - break; - - case DataKind.Party_List: - this.DrawPartyList(); - break; - - case DataKind.Buddy_List: - this.DrawBuddyList(); - break; - - case DataKind.Plugin_IPC: - this.DrawPluginIPC(); - break; - - case DataKind.Condition: - this.DrawCondition(); - break; - - case DataKind.Gauge: - this.DrawGauge(); - break; - - case DataKind.Command: - this.DrawCommand(); - break; - - case DataKind.Addon: - this.DrawAddon(); - break; - - case DataKind.Addon_Inspector: - this.DrawAddonInspector(); - break; - - case DataKind.AtkArrayData_Browser: - this.DrawAtkArrayDataBrowser(); - break; - - case DataKind.StartInfo: - this.DrawStartInfo(); - break; - - case DataKind.Target: - this.DrawTarget(); - break; - - case DataKind.Toast: - this.DrawToast(); - break; - - case DataKind.FlyText: - this.DrawFlyText(); - break; - - case DataKind.ImGui: - this.DrawImGui(); - break; - - case DataKind.Tex: - this.DrawTex(); - break; - - case DataKind.KeyState: - this.DrawKeyState(); - break; - - case DataKind.Gamepad: - this.DrawGamepad(); - break; - - case DataKind.Configuration: - this.DrawConfiguration(); - break; - - case DataKind.TaskSched: - this.DrawTaskSched(); - break; - - case DataKind.Hook: - this.DrawHook(); - break; - - case DataKind.Aetherytes: - this.DrawAetherytes(); - break; - - case DataKind.Dtr_Bar: - this.DrawDtr(); - break; - - case DataKind.UIColor: - this.DrawUIColor(); - break; - case DataKind.DataShare: - this.DrawDataShareTab(); - break; - } - } - else - { - ImGui.TextUnformatted("Data not ready."); - } - - this.isExcept = false; - } - catch (Exception ex) - { - if (!this.isExcept) - { - Log.Error(ex, "Could not draw data"); - } - - this.isExcept = true; - - ImGui.TextUnformatted(ex.ToString()); - } - - ImGui.PopStyleVar(); - - ImGui.EndChild(); - } - - private int MessageBoxWDetour(IntPtr hwnd, string text, string caption, NativeFunctions.MessageBoxType type) - { - Log.Information("[DATAHOOK] {0} {1} {2} {3}", hwnd, text, caption, type); - - var result = this.messageBoxMinHook.Original(hwnd, "Cause Access Violation?", caption, NativeFunctions.MessageBoxType.YesNo); - - if (result == (int)User32.MessageBoxResult.IDYES) - { - Marshal.ReadByte(IntPtr.Zero); - } - - return result; - } - - private void DrawServerOpCode() - { - ImGui.TextUnformatted(this.serverOpString); - } - - private void DrawAddress() - { - ImGui.InputText(".text sig", ref this.inputSig, 400); - if (ImGui.Button("Resolve")) - { - try - { - var sigScanner = Service.Get(); - this.sigResult = sigScanner.ScanText(this.inputSig); - } - catch (KeyNotFoundException) - { - this.sigResult = new IntPtr(-1); - } - } - - ImGui.Text($"Result: {this.sigResult.ToInt64():X}"); - ImGui.SameLine(); - if (ImGui.Button($"C{this.copyButtonIndex++}")) - ImGui.SetClipboardText(this.sigResult.ToInt64().ToString("x")); - - foreach (var debugScannedValue in BaseAddressResolver.DebugScannedValues) - { - ImGui.TextUnformatted($"{debugScannedValue.Key}"); - foreach (var valueTuple in debugScannedValue.Value) - { - ImGui.TextUnformatted( - $" {valueTuple.ClassName} - 0x{valueTuple.Address.ToInt64():x}"); - ImGui.SameLine(); - - if (ImGui.Button($"C##copyAddress{this.copyButtonIndex++}")) - ImGui.SetClipboardText(valueTuple.Address.ToInt64().ToString("x")); - } - } - } - - private void DrawObjectTable() - { - var chatGui = Service.Get(); - var clientState = Service.Get(); - var framework = Service.Get(); - var gameGui = Service.Get(); - var objectTable = Service.Get(); - - var stateString = string.Empty; - - if (clientState.LocalPlayer == null) - { - ImGui.TextUnformatted("LocalPlayer null."); - } - else if (clientState.IsPvPExcludingDen) - { - ImGui.TextUnformatted("Cannot access object table while in PvP."); - } - else - { - stateString += $"ObjectTableLen: {objectTable.Length}\n"; - stateString += $"LocalPlayerName: {clientState.LocalPlayer.Name}\n"; - stateString += $"CurrentWorldName: {(this.resolveGameData ? clientState.LocalPlayer.CurrentWorld.GameData.Name : clientState.LocalPlayer.CurrentWorld.Id.ToString())}\n"; - stateString += $"HomeWorldName: {(this.resolveGameData ? clientState.LocalPlayer.HomeWorld.GameData.Name : clientState.LocalPlayer.HomeWorld.Id.ToString())}\n"; - stateString += $"LocalCID: {clientState.LocalContentId:X}\n"; - stateString += $"LastLinkedItem: {chatGui.LastLinkedItemId}\n"; - stateString += $"TerritoryType: {clientState.TerritoryType}\n\n"; - - ImGui.TextUnformatted(stateString); - - ImGui.Checkbox("Draw characters on screen", ref this.drawCharas); - ImGui.SliderFloat("Draw Distance", ref this.maxCharaDrawDistance, 2f, 40f); - - for (var i = 0; i < objectTable.Length; i++) - { - var obj = objectTable[i]; - - if (obj == null) - continue; - - this.PrintGameObject(obj, i.ToString()); - - if (this.drawCharas && gameGui.WorldToScreen(obj.Position, out var screenCoords)) - { - // So, while WorldToScreen will return false if the point is off of game client screen, to - // to avoid performance issues, we have to manually determine if creating a window would - // produce a new viewport, and skip rendering it if so - var objectText = $"{obj.Address.ToInt64():X}:{obj.ObjectId:X}[{i}] - {obj.ObjectKind} - {obj.Name}"; - - var screenPos = ImGui.GetMainViewport().Pos; - var screenSize = ImGui.GetMainViewport().Size; - - var windowSize = ImGui.CalcTextSize(objectText); - - // Add some extra safety padding - windowSize.X += ImGui.GetStyle().WindowPadding.X + 10; - windowSize.Y += ImGui.GetStyle().WindowPadding.Y + 10; - - if (screenCoords.X + windowSize.X > screenPos.X + screenSize.X || - screenCoords.Y + windowSize.Y > screenPos.Y + screenSize.Y) - continue; - - if (obj.YalmDistanceX > this.maxCharaDrawDistance) - continue; - - ImGui.SetNextWindowPos(new Vector2(screenCoords.X, screenCoords.Y)); - - ImGui.SetNextWindowBgAlpha(Math.Max(1f - (obj.YalmDistanceX / this.maxCharaDrawDistance), 0.2f)); - if (ImGui.Begin( - $"Actor{i}##ActorWindow{i}", - ImGuiWindowFlags.NoDecoration | - ImGuiWindowFlags.AlwaysAutoResize | - ImGuiWindowFlags.NoSavedSettings | - ImGuiWindowFlags.NoMove | - ImGuiWindowFlags.NoMouseInputs | - ImGuiWindowFlags.NoDocking | - ImGuiWindowFlags.NoFocusOnAppearing | - ImGuiWindowFlags.NoNav)) - ImGui.Text(objectText); - ImGui.End(); - } - } - } - } - - private void DrawFateTable() - { - var fateTable = Service.Get(); - var framework = Service.Get(); - - var stateString = string.Empty; - if (fateTable.Length == 0) - { - ImGui.TextUnformatted("No fates or data not ready."); - } - else - { - stateString += $"FateTableLen: {fateTable.Length}\n"; - - ImGui.TextUnformatted(stateString); - - for (var i = 0; i < fateTable.Length; i++) - { - var fate = fateTable[i]; - if (fate == null) - continue; - - var fateString = $"{fate.Address.ToInt64():X}:[{i}]" + - $" - Lv.{fate.Level} {fate.Name} ({fate.Progress}%)" + - $" - X{fate.Position.X} Y{fate.Position.Y} Z{fate.Position.Z}" + - $" - Territory {(this.resolveGameData ? (fate.TerritoryType.GameData?.Name ?? fate.TerritoryType.Id.ToString()) : fate.TerritoryType.Id.ToString())}\n"; - - fateString += $" StartTimeEpoch: {fate.StartTimeEpoch}" + - $" - Duration: {fate.Duration}" + - $" - State: {fate.State}" + - $" - GameData name: {(this.resolveGameData ? (fate.GameData?.Name ?? fate.FateId.ToString()) : fate.FateId.ToString())}"; - - ImGui.TextUnformatted(fateString); - ImGui.SameLine(); - if (ImGui.Button("C")) - { - ImGui.SetClipboardText(fate.Address.ToString("X")); - } - } - } - } - - private void DrawSEFontTest() - { - var specialChars = string.Empty; - - for (var i = 0xE020; i <= 0xE0DB; i++) - specialChars += $"0x{i:X} - {(SeIconChar)i} - {(char)i}\n"; - - ImGui.TextUnformatted(specialChars); - } - - private void DrawFontAwesomeTest() - { - this.iconCategories ??= FontAwesomeHelpers.GetCategories(); - - if (this.iconSearchChanged) - { - this.icons = FontAwesomeHelpers.SearchIcons(this.iconSearchInput, this.iconCategories[this.selectedIconCategory]); - this.iconNames = this.icons.Select(icon => Enum.GetName(icon)!).ToList(); - this.iconSearchChanged = false; - } - - ImGui.SetNextItemWidth(160f); - var categoryIndex = this.selectedIconCategory; - if (ImGui.Combo("####FontAwesomeCategorySearch", ref categoryIndex, this.iconCategories, this.iconCategories.Length)) - { - this.selectedIconCategory = categoryIndex; - this.iconSearchChanged = true; - } - - ImGui.SameLine(170f); - ImGui.SetNextItemWidth(180f); - if (ImGui.InputTextWithHint($"###FontAwesomeInputSearch", "search icons", ref this.iconSearchInput, 50)) - { - this.iconSearchChanged = true; - } - - ImGuiHelpers.ScaledDummy(10f); - for (var i = 0; i < this.icons?.Count; i++) - { - ImGui.Text($"0x{(int)this.icons[i].ToIconChar():X}"); - ImGuiHelpers.ScaledRelativeSameLine(50f); - ImGui.Text($"{this.iconNames[i]}"); - ImGuiHelpers.ScaledRelativeSameLine(280f); - ImGui.PushFont(UiBuilder.IconFont); - ImGui.Text(this.icons[i].ToIconString()); - ImGui.PopFont(); - ImGuiHelpers.ScaledDummy(2f); - } - } - - private void DrawPartyList() - { - var partyList = Service.Get(); - - ImGui.Checkbox("Resolve Actors", ref this.resolveObjects); - - ImGui.Text($"GroupManager: {partyList.GroupManagerAddress.ToInt64():X}"); - ImGui.Text($"GroupList: {partyList.GroupListAddress.ToInt64():X}"); - ImGui.Text($"AllianceList: {partyList.AllianceListAddress.ToInt64():X}"); - - ImGui.Text($"{partyList.Length} Members"); - - for (var i = 0; i < partyList.Length; i++) - { - var member = partyList[i]; - if (member == null) - { - ImGui.Text($"[{i}] was null"); - continue; - } - - ImGui.Text($"[{i}] {member.Address.ToInt64():X} - {member.Name} - {member.GameObject.ObjectId}"); - if (this.resolveObjects) - { - var actor = member.GameObject; - if (actor == null) - { - ImGui.Text("Actor was null"); - } - else - { - this.PrintGameObject(actor, "-"); - } - } - } - } - - private void DrawBuddyList() - { - var buddyList = Service.Get(); - - ImGui.Checkbox("Resolve Actors", ref this.resolveObjects); - - ImGui.Text($"BuddyList: {buddyList.BuddyListAddress.ToInt64():X}"); - { - var member = buddyList.CompanionBuddy; - if (member == null) - { - ImGui.Text("[Companion] null"); - } - else - { - ImGui.Text($"[Companion] {member.Address.ToInt64():X} - {member.ObjectId} - {member.DataID}"); - if (this.resolveObjects) - { - var gameObject = member.GameObject; - if (gameObject == null) - { - ImGui.Text("GameObject was null"); - } - else - { - this.PrintGameObject(gameObject, "-"); - } - } - } - } - - { - var member = buddyList.PetBuddy; - if (member == null) - { - ImGui.Text("[Pet] null"); - } - else - { - ImGui.Text($"[Pet] {member.Address.ToInt64():X} - {member.ObjectId} - {member.DataID}"); - if (this.resolveObjects) - { - var gameObject = member.GameObject; - if (gameObject == null) - { - ImGui.Text("GameObject was null"); - } - else - { - this.PrintGameObject(gameObject, "-"); - } - } - } - } - - { - var count = buddyList.Length; - if (count == 0) - { - ImGui.Text("[BattleBuddy] None present"); - } - else - { - for (var i = 0; i < count; i++) - { - var member = buddyList[i]; - ImGui.Text($"[BattleBuddy] [{i}] {member.Address.ToInt64():X} - {member.ObjectId} - {member.DataID}"); - if (this.resolveObjects) - { - var gameObject = member.GameObject; - if (gameObject == null) - { - ImGui.Text("GameObject was null"); - } - else - { - this.PrintGameObject(gameObject, "-"); - } - } - } - } - } - } - - private void DrawPluginIPC() - { - if (this.ipcPub == null) - { - this.ipcPub = new CallGatePubSub("dataDemo1"); - - this.ipcPub.RegisterAction((msg) => - { - Log.Information($"Data action was called: {msg}"); - }); - - this.ipcPub.RegisterFunc((msg) => - { - Log.Information($"Data func was called: {msg}"); - return Guid.NewGuid().ToString(); - }); - } - - if (this.ipcSub == null) - { - this.ipcSub = new CallGatePubSub("dataDemo1"); - this.ipcSub.Subscribe((msg) => - { - Log.Information("PONG1"); - }); - this.ipcSub.Subscribe((msg) => - { - Log.Information("PONG2"); - }); - this.ipcSub.Subscribe((msg) => - { - throw new Exception("PONG3"); - }); - } - - if (ImGui.Button("PING")) - { - this.ipcPub.SendMessage("PING"); - } - - if (ImGui.Button("Action")) - { - this.ipcSub.InvokeAction("button1"); - } - - if (ImGui.Button("Func")) - { - this.callGateResponse = this.ipcSub.InvokeFunc("button2"); - } - - if (!this.callGateResponse.IsNullOrEmpty()) - ImGui.Text($"Response: {this.callGateResponse}"); - } - - private void DrawCondition() - { - var condition = Service.Get(); - -#if DEBUG - ImGui.Text($"ptr: 0x{condition.Address.ToInt64():X}"); -#endif - - ImGui.Text("Current Conditions:"); - ImGui.Separator(); - - var didAny = false; - - for (var i = 0; i < Condition.MaxConditionEntries; i++) - { - var typedCondition = (ConditionFlag)i; - var cond = condition[typedCondition]; - - if (!cond) continue; - - didAny = true; - - ImGui.Text($"ID: {i} Enum: {typedCondition}"); - } - - if (!didAny) - ImGui.Text("None. Talk to a shop NPC or visit a market board to find out more!!!!!!!"); - } - - private void DrawGauge() - { - var clientState = Service.Get(); - var jobGauges = Service.Get(); - - var player = clientState.LocalPlayer; - if (player == null) - { - ImGui.Text("Player is not present"); - return; - } - - var jobID = player.ClassJob.Id; - JobGaugeBase? gauge = jobID switch - { - 19 => jobGauges.Get(), - 20 => jobGauges.Get(), - 21 => jobGauges.Get(), - 22 => jobGauges.Get(), - 23 => jobGauges.Get(), - 24 => jobGauges.Get(), - 25 => jobGauges.Get(), - 27 => jobGauges.Get(), - 28 => jobGauges.Get(), - 30 => jobGauges.Get(), - 31 => jobGauges.Get(), - 32 => jobGauges.Get(), - 33 => jobGauges.Get(), - 34 => jobGauges.Get(), - 35 => jobGauges.Get(), - 37 => jobGauges.Get(), - 38 => jobGauges.Get(), - 39 => jobGauges.Get(), - 40 => jobGauges.Get(), - _ => null, - }; - - if (gauge == null) - { - ImGui.Text("No supported gauge exists for this job."); - return; - } - - Util.ShowObject(gauge); - } - - private void DrawCommand() - { - var commandManager = Service.Get(); - - foreach (var command in commandManager.Commands) - { - ImGui.Text($"{command.Key}\n -> {command.Value.HelpMessage}\n -> In help: {command.Value.ShowInHelp}\n\n"); - } - } - - private unsafe void DrawAddon() - { - var gameGui = Service.Get(); - - ImGui.InputText("Addon name", ref this.inputAddonName, 256); - ImGui.InputInt("Addon Index", ref this.inputAddonIndex); - - if (this.inputAddonName.IsNullOrEmpty()) - return; - - var address = gameGui.GetAddonByName(this.inputAddonName, this.inputAddonIndex); - - if (address == IntPtr.Zero) - { - ImGui.Text("Null"); - return; - } - - var addon = (FFXIVClientStructs.FFXIV.Component.GUI.AtkUnitBase*)address; - var name = MemoryHelper.ReadStringNullTerminated((IntPtr)addon->Name); - ImGui.TextUnformatted($"{name} - 0x{address.ToInt64():x}\n v:{addon->IsVisible} x:{addon->X} y:{addon->Y} s:{addon->Scale}, w:{addon->RootNode->Width}, h:{addon->RootNode->Height}"); - - if (ImGui.Button("Find Agent")) - { - this.findAgentInterfacePtr = gameGui.FindAgentInterface(address); - } - - if (this.findAgentInterfacePtr != IntPtr.Zero) - { - ImGui.TextUnformatted($"Agent: 0x{this.findAgentInterfacePtr.ToInt64():x}"); - ImGui.SameLine(); - - if (ImGui.Button("C")) - ImGui.SetClipboardText(this.findAgentInterfacePtr.ToInt64().ToString("x")); - } - } - - private void DrawAddonInspector() - { - this.addonInspector ??= new UiDebug(); - this.addonInspector.Draw(); - } - - private unsafe void DrawAtkArrayDataBrowser() - { - var fontWidth = ImGui.CalcTextSize("A").X; - var fontHeight = ImGui.GetTextLineHeightWithSpacing(); - var uiModule = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()->GetUiModule(); - - if (uiModule == null) - { - ImGui.Text("UIModule unavailable."); - return; - } - - var atkArrayDataHolder = &uiModule->GetRaptureAtkModule()->AtkModule.AtkArrayDataHolder; - - if (ImGui.BeginTabBar("AtkArrayDataBrowserTabBar")) - { - if (ImGui.BeginTabItem($"NumberArrayData [{atkArrayDataHolder->NumberArrayCount}]")) - { - if (ImGui.BeginTable("NumberArrayDataTable", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY)) - { - ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed, fontWidth * 10); - ImGui.TableSetupColumn("Size", ImGuiTableColumnFlags.WidthFixed, fontWidth * 10); - ImGui.TableSetupColumn("Pointer", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableHeadersRow(); - for (var numberArrayIndex = 0; numberArrayIndex < atkArrayDataHolder->NumberArrayCount; numberArrayIndex++) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text($"{numberArrayIndex} [{numberArrayIndex * 8:X}]"); - ImGui.TableNextColumn(); - var numberArrayData = atkArrayDataHolder->NumberArrays[numberArrayIndex]; - if (numberArrayData != null) - { - ImGui.Text($"{numberArrayData->AtkArrayData.Size}"); - ImGui.TableNextColumn(); - if (ImGui.TreeNodeEx($"{(long)numberArrayData:X}###{numberArrayIndex}", ImGuiTreeNodeFlags.SpanFullWidth)) - { - ImGui.NewLine(); - var tableHeight = Math.Min(40, numberArrayData->AtkArrayData.Size + 4); - if (ImGui.BeginTable($"NumberArrayDataTable", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, new Vector2(0.0F, fontHeight * tableHeight))) - { - ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed, fontWidth * 6); - ImGui.TableSetupColumn("Hex", ImGuiTableColumnFlags.WidthFixed, fontWidth * 9); - ImGui.TableSetupColumn("Integer", ImGuiTableColumnFlags.WidthFixed, fontWidth * 12); - ImGui.TableSetupColumn("Float", ImGuiTableColumnFlags.WidthFixed, fontWidth * 20); - ImGui.TableHeadersRow(); - for (var numberIndex = 0; numberIndex < numberArrayData->AtkArrayData.Size; numberIndex++) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text($"{numberIndex}"); - ImGui.TableNextColumn(); - ImGui.Text($"{numberArrayData->IntArray[numberIndex]:X}"); - ImGui.TableNextColumn(); - ImGui.Text($"{numberArrayData->IntArray[numberIndex]}"); - ImGui.TableNextColumn(); - ImGui.Text($"{*(float*)&numberArrayData->IntArray[numberIndex]}"); - } - - ImGui.EndTable(); - } - - ImGui.TreePop(); - } - } - else - { - ImGui.TextDisabled("--"); - ImGui.TableNextColumn(); - ImGui.TextDisabled("--"); - } - } - - ImGui.EndTable(); - } - - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem($"StringArrayData [{atkArrayDataHolder->StringArrayCount}]")) - { - if (ImGui.BeginTable("StringArrayDataTable", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY)) - { - ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed, fontWidth * 10); - ImGui.TableSetupColumn("Size", ImGuiTableColumnFlags.WidthFixed, fontWidth * 10); - ImGui.TableSetupColumn("Pointer", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableHeadersRow(); - for (var stringArrayIndex = 0; stringArrayIndex < atkArrayDataHolder->StringArrayCount; stringArrayIndex++) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text($"{stringArrayIndex} [{stringArrayIndex * 8:X}]"); - ImGui.TableNextColumn(); - var stringArrayData = atkArrayDataHolder->StringArrays[stringArrayIndex]; - if (stringArrayData != null) - { - ImGui.Text($"{stringArrayData->AtkArrayData.Size}"); - ImGui.TableNextColumn(); - if (ImGui.TreeNodeEx($"{(long)stringArrayData:X}###{stringArrayIndex}", ImGuiTreeNodeFlags.SpanFullWidth)) - { - ImGui.NewLine(); - var tableHeight = Math.Min(40, stringArrayData->AtkArrayData.Size + 4); - if (ImGui.BeginTable($"StringArrayDataTable", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY, new Vector2(0.0F, fontHeight * tableHeight))) - { - ImGui.TableSetupColumn("Index", ImGuiTableColumnFlags.WidthFixed, fontWidth * 6); - ImGui.TableSetupColumn("String", ImGuiTableColumnFlags.WidthStretch); - ImGui.TableHeadersRow(); - for (var stringIndex = 0; stringIndex < stringArrayData->AtkArrayData.Size; stringIndex++) - { - ImGui.TableNextRow(); - ImGui.TableNextColumn(); - ImGui.Text($"{stringIndex}"); - ImGui.TableNextColumn(); - if (stringArrayData->StringArray[stringIndex] != null) - { - ImGui.Text($"{MemoryHelper.ReadSeStringNullTerminated(new IntPtr(stringArrayData->StringArray[stringIndex]))}"); - } - else - { - ImGui.TextDisabled("--"); - } - } - - ImGui.EndTable(); - } - - ImGui.TreePop(); - } - } - else - { - ImGui.TextDisabled("--"); - ImGui.TableNextColumn(); - ImGui.TextDisabled("--"); - } - } - - ImGui.EndTable(); - } - - ImGui.EndTabItem(); - } - - ImGui.EndTabBar(); - } - } - - private void DrawStartInfo() - { - var startInfo = Service.Get(); - - ImGui.Text(JsonConvert.SerializeObject(startInfo, Formatting.Indented)); - } - - private void DrawTarget() - { - var clientState = Service.Get(); - var targetMgr = Service.Get(); - - if (targetMgr.Target != null) - { - this.PrintGameObject(targetMgr.Target, "CurrentTarget"); - - ImGui.Text("Target"); - Util.ShowGameObjectStruct(targetMgr.Target); - - var tot = targetMgr.Target.TargetObject; - if (tot != null) - { - ImGuiHelpers.ScaledDummy(10); - - ImGui.Separator(); - ImGui.Text("ToT"); - Util.ShowGameObjectStruct(tot); - } - - ImGuiHelpers.ScaledDummy(10); - } - - if (targetMgr.FocusTarget != null) - this.PrintGameObject(targetMgr.FocusTarget, "FocusTarget"); - - if (targetMgr.MouseOverTarget != null) - this.PrintGameObject(targetMgr.MouseOverTarget, "MouseOverTarget"); - - if (targetMgr.PreviousTarget != null) - this.PrintGameObject(targetMgr.PreviousTarget, "PreviousTarget"); - - if (targetMgr.SoftTarget != null) - this.PrintGameObject(targetMgr.SoftTarget, "SoftTarget"); - - if (ImGui.Button("Clear CT")) - targetMgr.ClearTarget(); - - if (ImGui.Button("Clear FT")) - targetMgr.ClearFocusTarget(); - - var localPlayer = clientState.LocalPlayer; - - if (localPlayer != null) - { - if (ImGui.Button("Set CT")) - targetMgr.SetTarget(localPlayer); - - if (ImGui.Button("Set FT")) - targetMgr.SetFocusTarget(localPlayer); - } - else - { - ImGui.Text("LocalPlayer is null."); - } - } - - private void DrawToast() - { - var toastGui = Service.Get(); - - ImGui.InputText("Toast text", ref this.inputTextToast, 200); - - ImGui.Combo("Toast Position", ref this.toastPosition, new[] { "Bottom", "Top", }, 2); - ImGui.Combo("Toast Speed", ref this.toastSpeed, new[] { "Slow", "Fast", }, 2); - ImGui.Combo("Quest Toast Position", ref this.questToastPosition, new[] { "Centre", "Right", "Left" }, 3); - ImGui.Checkbox("Quest Checkmark", ref this.questToastCheckmark); - ImGui.Checkbox("Quest Play Sound", ref this.questToastSound); - ImGui.InputInt("Quest Icon ID", ref this.questToastIconId); - - ImGuiHelpers.ScaledDummy(new Vector2(10, 10)); - - if (ImGui.Button("Show toast")) - { - toastGui.ShowNormal(this.inputTextToast, new ToastOptions - { - Position = (ToastPosition)this.toastPosition, - Speed = (ToastSpeed)this.toastSpeed, - }); - } - - if (ImGui.Button("Show Quest toast")) - { - toastGui.ShowQuest(this.inputTextToast, new QuestToastOptions - { - Position = (QuestToastPosition)this.questToastPosition, - DisplayCheckmark = this.questToastCheckmark, - IconId = (uint)this.questToastIconId, - PlaySound = this.questToastSound, - }); - } - - if (ImGui.Button("Show Error toast")) - { - toastGui.ShowError(this.inputTextToast); - } - } - - private void DrawFlyText() - { - if (ImGui.BeginCombo("Kind", this.flyKind.ToString())) - { - var names = Enum.GetNames(typeof(FlyTextKind)); - for (var i = 0; i < names.Length; i++) - { - if (ImGui.Selectable($"{names[i]} ({i})")) - this.flyKind = (FlyTextKind)i; - } - - ImGui.EndCombo(); - } - - ImGui.InputText("Text1", ref this.flyText1, 200); - ImGui.InputText("Text2", ref this.flyText2, 200); - - ImGui.InputInt("Val1", ref this.flyVal1); - ImGui.InputInt("Val2", ref this.flyVal2); - - ImGui.InputInt("Icon ID", ref this.flyIcon); - ImGui.InputInt("Damage Icon ID", ref this.flyDmgIcon); - ImGui.ColorEdit4("Color", ref this.flyColor); - ImGui.InputInt("Actor Index", ref this.flyActor); - var sendColor = ImGui.ColorConvertFloat4ToU32(this.flyColor); - - if (ImGui.Button("Send")) - { - Service.Get().AddFlyText( - this.flyKind, - unchecked((uint)this.flyActor), - unchecked((uint)this.flyVal1), - unchecked((uint)this.flyVal2), - this.flyText1, - this.flyText2, - sendColor, - unchecked((uint)this.flyIcon), - unchecked((uint)this.flyDmgIcon)); - } - } - - private void DrawImGui() - { - var interfaceManager = Service.Get(); - var notifications = Service.Get(); - - ImGui.Text("Monitor count: " + ImGui.GetPlatformIO().Monitors.Size); - ImGui.Text("OverrideGameCursor: " + interfaceManager.OverrideGameCursor); - - ImGui.Button("THIS IS A BUTTON###hoverTestButton"); - interfaceManager.OverrideGameCursor = !ImGui.IsItemHovered(); - - ImGui.Separator(); - - ImGui.TextUnformatted($"WindowSystem.TimeSinceLastAnyFocus: {WindowSystem.TimeSinceLastAnyFocus.TotalMilliseconds:0}ms"); - - ImGui.Separator(); - - if (ImGui.Button("Add random notification")) - { - var rand = new Random(); - - var title = rand.Next(0, 5) switch - { - 0 => "This is a toast", - 1 => "Truly, a toast", - 2 => "I am testing this toast", - 3 => "I hope this looks right", - 4 => "Good stuff", - 5 => "Nice", - _ => null, - }; - - var type = rand.Next(0, 4) switch - { - 0 => Notifications.NotificationType.Error, - 1 => Notifications.NotificationType.Warning, - 2 => Notifications.NotificationType.Info, - 3 => Notifications.NotificationType.Success, - 4 => Notifications.NotificationType.None, - _ => Notifications.NotificationType.None, - }; - - var text = "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; - - notifications.AddNotification(text, title, type); - } - } - - private void DrawTex() - { - var dataManager = Service.Get(); - - ImGui.InputText("Tex Path", ref this.inputTexPath, 255); - ImGui.InputFloat2("UV0", ref this.inputTexUv0); - ImGui.InputFloat2("UV1", ref this.inputTexUv1); - ImGui.InputFloat4("Tint", ref this.inputTintCol); - ImGui.InputFloat2("Scale", ref this.inputTexScale); - - if (ImGui.Button("Load Tex")) - { - try - { - this.debugTex = dataManager.GetImGuiTexture(this.inputTexPath); - this.inputTexScale = new Vector2(this.debugTex.Width, this.debugTex.Height); - } - catch (Exception ex) - { - Log.Error(ex, "Could not load tex."); - } - } - - ImGuiHelpers.ScaledDummy(10); - - if (this.debugTex != null) - { - ImGui.Image(this.debugTex.ImGuiHandle, this.inputTexScale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol); - ImGuiHelpers.ScaledDummy(5); - Util.ShowObject(this.debugTex); - } - } - - private void DrawKeyState() - { - var keyState = Service.Get(); - - ImGui.Columns(4); - - var i = 0; - foreach (var vkCode in keyState.GetValidVirtualKeys()) - { - var code = (int)vkCode; - var value = keyState[code]; - - ImGui.PushStyleColor(ImGuiCol.Text, value ? ImGuiColors.HealerGreen : ImGuiColors.DPSRed); - - ImGui.Text($"{vkCode} ({code})"); - - ImGui.PopStyleColor(); - - i++; - if (i % 24 == 0) - ImGui.NextColumn(); - } - - ImGui.Columns(1); - } - - private void DrawGamepad() - { - var gamepadState = Service.Get(); - - static void DrawHelper(string text, uint mask, Func resolve) - { - ImGui.Text($"{text} {mask:X4}"); - ImGui.Text($"DPadLeft {resolve(GamepadButtons.DpadLeft)} " + - $"DPadUp {resolve(GamepadButtons.DpadUp)} " + - $"DPadRight {resolve(GamepadButtons.DpadRight)} " + - $"DPadDown {resolve(GamepadButtons.DpadDown)} "); - ImGui.Text($"West {resolve(GamepadButtons.West)} " + - $"North {resolve(GamepadButtons.North)} " + - $"East {resolve(GamepadButtons.East)} " + - $"South {resolve(GamepadButtons.South)} "); - ImGui.Text($"L1 {resolve(GamepadButtons.L1)} " + - $"L2 {resolve(GamepadButtons.L2)} " + - $"R1 {resolve(GamepadButtons.R1)} " + - $"R2 {resolve(GamepadButtons.R2)} "); - ImGui.Text($"Select {resolve(GamepadButtons.Select)} " + - $"Start {resolve(GamepadButtons.Start)} " + - $"L3 {resolve(GamepadButtons.L3)} " + - $"R3 {resolve(GamepadButtons.R3)} "); - } - - ImGui.Text($"GamepadInput 0x{gamepadState.GamepadInputAddress.ToInt64():X}"); - -#if DEBUG - if (ImGui.IsItemHovered()) - ImGui.SetMouseCursor(ImGuiMouseCursor.Hand); - - if (ImGui.IsItemClicked()) - ImGui.SetClipboardText($"0x{gamepadState.GamepadInputAddress.ToInt64():X}"); -#endif - - DrawHelper( - "Buttons Raw", - gamepadState.ButtonsRaw, - gamepadState.Raw); - DrawHelper( - "Buttons Pressed", - gamepadState.ButtonsPressed, - gamepadState.Pressed); - DrawHelper( - "Buttons Repeat", - gamepadState.ButtonsRepeat, - gamepadState.Repeat); - DrawHelper( - "Buttons Released", - gamepadState.ButtonsReleased, - gamepadState.Released); - ImGui.Text($"LeftStickLeft {gamepadState.LeftStickLeft:0.00} " + - $"LeftStickUp {gamepadState.LeftStickUp:0.00} " + - $"LeftStickRight {gamepadState.LeftStickRight:0.00} " + - $"LeftStickDown {gamepadState.LeftStickDown:0.00} "); - ImGui.Text($"RightStickLeft {gamepadState.RightStickLeft:0.00} " + - $"RightStickUp {gamepadState.RightStickUp:0.00} " + - $"RightStickRight {gamepadState.RightStickRight:0.00} " + - $"RightStickDown {gamepadState.RightStickDown:0.00} "); - } - - private void DrawConfiguration() - { - var config = Service.Get(); - Util.ShowObject(config); - } - - private void DrawTaskSched() - { - if (ImGui.Button("Clear list")) - { - TaskTracker.Clear(); - } - - ImGui.SameLine(); - ImGuiHelpers.ScaledDummy(10); - ImGui.SameLine(); - - if (ImGui.Button("Cancel using CancellationTokenSource")) - { - this.taskSchedCancelSource.Cancel(); - this.taskSchedCancelSource = new(); - } - - ImGui.Text("Run in any thread: "); - ImGui.SameLine(); - - if (ImGui.Button("Short Task.Run")) - { - Task.Run(() => { Thread.Sleep(500); }); - } - - ImGui.SameLine(); - - if (ImGui.Button("Task in task(Delay)")) - { - var token = this.taskSchedCancelSource.Token; - Task.Run(async () => await this.TestTaskInTaskDelay(token)); - } - - ImGui.SameLine(); - - if (ImGui.Button("Task in task(Sleep)")) - { - Task.Run(async () => await this.TestTaskInTaskSleep()); - } - - ImGui.SameLine(); - - if (ImGui.Button("Faulting task")) - { - Task.Run(() => - { - Thread.Sleep(200); - - string a = null; - a.Contains("dalamud"); - }); - } - - ImGui.Text("Run in Framework.Update: "); - ImGui.SameLine(); - - if (ImGui.Button("ASAP")) - { - Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token)); - } - - ImGui.SameLine(); - - if (ImGui.Button("In 1s")) - { - Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token, delay: TimeSpan.FromSeconds(1))); - } - - ImGui.SameLine(); - - if (ImGui.Button("In 60f")) - { - Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedCancelSource.Token, delayTicks: 60)); - } - - ImGui.SameLine(); - - if (ImGui.Button("Error in 1s")) - { - Task.Run(async () => await Service.Get().RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedCancelSource.Token, delay: TimeSpan.FromSeconds(1))); - } - - ImGui.SameLine(); - - if (ImGui.Button("As long as it's in Framework Thread")) - { - Task.Run(async () => await Service.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); })); - Service.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait(); - } - - if (ImGui.Button("Drown in tasks")) - { - var token = this.taskSchedCancelSource.Token; - Task.Run(() => - { - for (var i = 0; i < 100; i++) - { - token.ThrowIfCancellationRequested(); - Task.Run(() => - { - for (var i = 0; i < 100; i++) - { - token.ThrowIfCancellationRequested(); - Task.Run(() => - { - for (var i = 0; i < 100; i++) - { - token.ThrowIfCancellationRequested(); - Task.Run(() => - { - for (var i = 0; i < 100; i++) - { - token.ThrowIfCancellationRequested(); - Task.Run(async () => - { - for (var i = 0; i < 100; i++) - { - token.ThrowIfCancellationRequested(); - await Task.Delay(1); - } - }); - } - }); - } - }); - } - }); - } - }); - } - - ImGui.SameLine(); - - ImGuiHelpers.ScaledDummy(20); - - // Needed to init the task tracker, if we're not on a debug build - Service.Get().Enable(); - - for (var i = 0; i < TaskTracker.Tasks.Count; i++) - { - var task = TaskTracker.Tasks[i]; - var subTime = DateTime.Now; - if (task.Task == null) - subTime = task.FinishTime; - - switch (task.Status) - { - case TaskStatus.Created: - case TaskStatus.WaitingForActivation: - case TaskStatus.WaitingToRun: - ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.DalamudGrey); - break; - case TaskStatus.Running: - case TaskStatus.WaitingForChildrenToComplete: - ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedBlue); - break; - case TaskStatus.RanToCompletion: - ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedGreen); - break; - case TaskStatus.Canceled: - case TaskStatus.Faulted: - ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.DalamudRed); - break; - default: - throw new ArgumentOutOfRangeException(); - } - - if (ImGui.CollapsingHeader($"#{task.Id} - {task.Status} {(subTime - task.StartTime).TotalMilliseconds}ms###task{i}")) - { - task.IsBeingViewed = true; - - if (ImGui.Button("CANCEL (May not work)")) - { - try - { - var cancelFunc = - typeof(Task).GetMethod("InternalCancel", BindingFlags.NonPublic | BindingFlags.Instance); - cancelFunc.Invoke(task, null); - } - catch (Exception ex) - { - Log.Error(ex, "Could not cancel task."); - } - } - - ImGuiHelpers.ScaledDummy(10); - - ImGui.TextUnformatted(task.StackTrace.ToString()); - - if (task.Exception != null) - { - ImGuiHelpers.ScaledDummy(15); - ImGui.TextColored(ImGuiColors.DalamudRed, "EXCEPTION:"); - ImGui.TextUnformatted(task.Exception.ToString()); - } - } - else - { - task.IsBeingViewed = false; - } - - ImGui.PopStyleColor(1); - } - } - - private void DrawHook() - { - try - { - ImGui.Checkbox("Use MinHook", ref this.hookUseMinHook); - - if (ImGui.Button("Create")) - this.messageBoxMinHook = Hook.FromSymbol("User32", "MessageBoxW", this.MessageBoxWDetour, this.hookUseMinHook); - - if (ImGui.Button("Enable")) - this.messageBoxMinHook?.Enable(); - - if (ImGui.Button("Disable")) - this.messageBoxMinHook?.Disable(); - - if (ImGui.Button("Call Original")) - this.messageBoxMinHook?.Original(IntPtr.Zero, "Hello from .Original", "Hook Test", NativeFunctions.MessageBoxType.Ok); - - if (ImGui.Button("Dispose")) - { - this.messageBoxMinHook?.Dispose(); - this.messageBoxMinHook = null; - } - - if (ImGui.Button("Test")) - _ = NativeFunctions.MessageBoxW(IntPtr.Zero, "Hi", "Hello", NativeFunctions.MessageBoxType.Ok); - - if (this.messageBoxMinHook != null) - ImGui.Text("Enabled: " + this.messageBoxMinHook?.IsEnabled); - } - catch (Exception ex) - { - Log.Error(ex, "MinHook error caught"); - } - } - - private void DrawAetherytes() - { - if (!ImGui.BeginTable("##aetheryteTable", 11, ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders)) - return; - - ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableSetupColumn("Idx", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("ID", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Zone", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Ward", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Plot", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Sub", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Gil", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Fav", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Shared", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableSetupColumn("Appartment", ImGuiTableColumnFlags.WidthFixed); - ImGui.TableHeadersRow(); - - var tpList = Service.Get(); - - for (var i = 0; i < tpList.Length; i++) - { - var info = tpList[i]; - if (info == null) - continue; - - ImGui.TableNextColumn(); // Idx - ImGui.TextUnformatted($"{i}"); - - ImGui.TableNextColumn(); // Name - ImGui.TextUnformatted($"{info.AetheryteData.GameData.PlaceName.Value?.Name}"); - - ImGui.TableNextColumn(); // ID - ImGui.TextUnformatted($"{info.AetheryteId}"); - - ImGui.TableNextColumn(); // Zone - ImGui.TextUnformatted($"{info.TerritoryId}"); - - ImGui.TableNextColumn(); // Ward - ImGui.TextUnformatted($"{info.Ward}"); - - ImGui.TableNextColumn(); // Plot - ImGui.TextUnformatted($"{info.Plot}"); - - ImGui.TableNextColumn(); // Sub - ImGui.TextUnformatted($"{info.SubIndex}"); - - ImGui.TableNextColumn(); // Gil - ImGui.TextUnformatted($"{info.GilCost}"); - - ImGui.TableNextColumn(); // Favourite - ImGui.TextUnformatted($"{info.IsFavourite}"); - - ImGui.TableNextColumn(); // Shared - ImGui.TextUnformatted($"{info.IsSharedHouse}"); - - ImGui.TableNextColumn(); // Appartment - ImGui.TextUnformatted($"{info.IsAppartment}"); - } - - ImGui.EndTable(); - } - - private void DrawDtr() - { - this.DrawDtrTestEntry(ref this.dtrTest1, "DTR Test #1"); - ImGui.Separator(); - this.DrawDtrTestEntry(ref this.dtrTest2, "DTR Test #2"); - ImGui.Separator(); - this.DrawDtrTestEntry(ref this.dtrTest3, "DTR Test #3"); - ImGui.Separator(); - - var configuration = Service.Get(); - if (configuration.DtrOrder != null) - { - ImGui.Separator(); - - foreach (var order in configuration.DtrOrder) - { - ImGui.Text(order); - } - } - } - - private void DrawDtrTestEntry(ref DtrBarEntry? entry, string title) - { - var dtrBar = Service.Get(); - - if (entry != null) - { - ImGui.Text(title); - - var text = entry.Text?.TextValue ?? string.Empty; - if (ImGui.InputText($"Text###{entry.Title}t", ref text, 255)) - entry.Text = text; - - var shown = entry.Shown; - if (ImGui.Checkbox($"Shown###{entry.Title}s", ref shown)) - entry.Shown = shown; - - if (ImGui.Button($"Remove###{entry.Title}r")) - { - entry.Remove(); - entry = null; - } - } - else - { - if (ImGui.Button($"Add###{title}")) - { - entry = dtrBar.Get(title, title); - } - } - } - - private void DrawDataShareTab() - { - if (!ImGui.BeginTable("###DataShareTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg)) - return; - - try - { - ImGui.TableSetupColumn("Shared Tag"); - ImGui.TableSetupColumn("Creator Assembly"); - ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Consumers"); - ImGui.TableHeadersRow(); - foreach (var share in Service.Get().GetAllShares()) - { - ImGui.TableNextColumn(); - ImGui.TextUnformatted(share.Tag); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(share.CreatorAssembly); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(share.Users.Length.ToString()); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(string.Join(", ", share.Users)); - } - } - finally - { - ImGui.EndTable(); - } - } - - private void DrawUIColor() - { - var colorSheet = Service.Get().GetExcelSheet(); - if (colorSheet is null) return; - - foreach (var color in colorSheet) - { - this.DrawUiColor(color); - } - } - - private void DrawUiColor(UIColor color) - { - ImGui.Text($"[{color.RowId:D3}] "); - ImGui.SameLine(); - ImGui.TextColored(this.ConvertToVector4(color.Unknown2), $"Unknown2 "); - ImGui.SameLine(); - ImGui.TextColored(this.ConvertToVector4(color.UIForeground), "UIForeground "); - ImGui.SameLine(); - ImGui.TextColored(this.ConvertToVector4(color.Unknown3), "Unknown3 "); - ImGui.SameLine(); - ImGui.TextColored(this.ConvertToVector4(color.UIGlow), "UIGlow"); - } - - private Vector4 ConvertToVector4(uint color) - { - var r = (byte)(color >> 24); - var g = (byte)(color >> 16); - var b = (byte)(color >> 8); - var a = (byte)color; - - return new Vector4(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f); - } - - private async Task TestTaskInTaskDelay(CancellationToken token) - { - await Task.Delay(5000, token); - } - -#pragma warning disable 1998 - private async Task TestTaskInTaskSleep() -#pragma warning restore 1998 - { - Thread.Sleep(5000); - } - - private void Load() - { - var dataManager = Service.Get(); - - if (dataManager.IsDataReady) - { - this.serverOpString = JsonConvert.SerializeObject(dataManager.ServerOpCodes, Formatting.Indented); - this.wasReady = true; - } - } - - private void PrintGameObject(GameObject actor, string tag) - { - var actorString = - $"{actor.Address.ToInt64():X}:{actor.ObjectId:X}[{tag}] - {actor.ObjectKind} - {actor.Name} - X{actor.Position.X} Y{actor.Position.Y} Z{actor.Position.Z} D{actor.YalmDistanceX} R{actor.Rotation} - Target: {actor.TargetObjectId:X}\n"; - - if (actor is Npc npc) - actorString += $" DataId: {npc.DataId} NameId:{npc.NameId}\n"; - - if (actor is Character chara) - { - actorString += - $" Level: {chara.Level} ClassJob: {(this.resolveGameData ? chara.ClassJob.GameData.Name : chara.ClassJob.Id.ToString())} CHP: {chara.CurrentHp} MHP: {chara.MaxHp} CMP: {chara.CurrentMp} MMP: {chara.MaxMp}\n Customize: {BitConverter.ToString(chara.Customize).Replace("-", " ")} StatusFlags: {chara.StatusFlags}\n"; - } - - if (actor is PlayerCharacter pc) - { - actorString += - $" HomeWorld: {(this.resolveGameData ? pc.HomeWorld.GameData.Name : pc.HomeWorld.Id.ToString())} CurrentWorld: {(this.resolveGameData ? pc.CurrentWorld.GameData.Name : pc.CurrentWorld.Id.ToString())} FC: {pc.CompanyTag}\n"; - } - - ImGui.TextUnformatted(actorString); - ImGui.SameLine(); - if (ImGui.Button($"C##{this.copyButtonIndex++}")) - { - ImGui.SetClipboardText(actor.Address.ToInt64().ToString("X")); - } - } -} diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 4b8fe6822..60b1901d0 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -13,6 +13,7 @@ using System.Text; using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; +using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; using Dalamud.Interface.Colors; @@ -36,7 +37,7 @@ public static class Util private static ulong moduleStartAddr; private static ulong moduleEndAddr; - + /// /// Gets an httpclient for usage. /// Do NOT await this. @@ -672,6 +673,40 @@ public static class Util return names.ElementAt(rng.Next(0, names.Count() - 1)).Singular.RawString; } + /// + /// Print formatted GameObject Information to ImGui + /// + /// Game Object to Display. + /// Display Tag. + /// If the GameObjects data should be resolved. + internal static void PrintGameObject(GameObject actor, string tag, bool resolveGameData) + { + var actorString = + $"{actor.Address.ToInt64():X}:{actor.ObjectId:X}[{tag}] - {actor.ObjectKind} - {actor.Name} - X{actor.Position.X} Y{actor.Position.Y} Z{actor.Position.Z} D{actor.YalmDistanceX} R{actor.Rotation} - Target: {actor.TargetObjectId:X}\n"; + + if (actor is Npc npc) + actorString += $" DataId: {npc.DataId} NameId:{npc.NameId}\n"; + + if (actor is Character chara) + { + actorString += + $" Level: {chara.Level} ClassJob: {(resolveGameData ? chara.ClassJob.GameData?.Name : chara.ClassJob.Id.ToString())} CHP: {chara.CurrentHp} MHP: {chara.MaxHp} CMP: {chara.CurrentMp} MMP: {chara.MaxMp}\n Customize: {BitConverter.ToString(chara.Customize).Replace("-", " ")} StatusFlags: {chara.StatusFlags}\n"; + } + + if (actor is PlayerCharacter pc) + { + actorString += + $" HomeWorld: {(resolveGameData ? pc.HomeWorld.GameData?.Name : pc.HomeWorld.Id.ToString())} CurrentWorld: {(resolveGameData ? pc.CurrentWorld.GameData?.Name : pc.CurrentWorld.Id.ToString())} FC: {pc.CompanyTag}\n"; + } + + ImGui.TextUnformatted(actorString); + ImGui.SameLine(); + if (ImGui.Button($"C##{actor.Address.ToInt64()}")) + { + ImGui.SetClipboardText(actor.Address.ToInt64().ToString("X")); + } + } + private static unsafe void ShowValue(ulong addr, IEnumerable path, Type type, object value) { if (type.IsPointer) From 221bef8df8f95c503cd75da9a8274136b79d5cc9 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 23 Jun 2023 21:22:35 +0200 Subject: [PATCH 24/28] fix: don't adjust boot log path in release mode --- Dalamud.Injector/EntryPoint.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 244b8e3a7..feed79772 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -118,15 +118,15 @@ namespace Dalamud.Injector private static string GetLogPath(string? baseDirectory, string fileName, string? logName) { baseDirectory ??= Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + baseDirectory ??= Environment.CurrentDirectory; fileName = !string.IsNullOrEmpty(logName) ? $"{fileName}-{logName}.log" : $"{fileName}.log"; -#if DEBUG - var logPath = Path.Combine(baseDirectory, fileName); -#else - var logPath = Path.Combine(baseDirectory, "..", "..", "..", fileName); -#endif + // TODO(api9): remove + var previousLogPath = Path.Combine(baseDirectory, "..", "..", "..", fileName); + if (File.Exists(previousLogPath)) + File.Delete(previousLogPath); - return logPath; + return Path.Combine(baseDirectory, fileName); } private static void Init(List args) From b80d9da1aaab1991bc289b8ded130d9f4fbbcb73 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 23 Jun 2023 21:23:52 +0200 Subject: [PATCH 25/28] build: 7.7.3.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index d0ac3afae..9629a7ec0 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.7.2.0 + 7.7.3.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 895c4ae443bf88bb24639678606b3654dbdd11e9 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 23 Jun 2023 17:25:37 -0700 Subject: [PATCH 26/28] Add functions to get High Resolution icons (#1259) Add new methods to retrieve high resolution icons (`_hr1`) from game files. --- Dalamud/Data/DataManager.cs | 89 +++++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 28 deletions(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index b6570a2ea..9d6a352ca 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -30,6 +30,7 @@ namespace Dalamud.Data; public sealed class DataManager : IDisposable, IServiceType { private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; + private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex"; private readonly Thread luminaResourceThread; private readonly CancellationTokenSource luminaCancellationTokenSource; @@ -169,10 +170,8 @@ public sealed class DataManager : IDisposable, IServiceType /// /// The excel sheet type to get. /// The , giving access to game rows. - public ExcelSheet? GetExcelSheet() where T : ExcelRow - { - return this.Excel.GetSheet(); - } + public ExcelSheet? GetExcelSheet() where T : ExcelRow + => this.Excel.GetSheet(); /// /// Get an with the given Excel sheet row type with a specified language. @@ -180,20 +179,16 @@ public sealed class DataManager : IDisposable, IServiceType /// Language of the sheet to get. /// The excel sheet type to get. /// The , giving access to game rows. - public ExcelSheet? GetExcelSheet(ClientLanguage language) where T : ExcelRow - { - return this.Excel.GetSheet(language.ToLumina()); - } + public ExcelSheet? GetExcelSheet(ClientLanguage language) where T : ExcelRow + => this.Excel.GetSheet(language.ToLumina()); /// /// Get a with the given path. /// /// The path inside of the game files. /// The of the file. - public FileResource? GetFile(string path) - { - return this.GetFile(path); - } + public FileResource? GetFile(string path) + => this.GetFile(path); /// /// Get a with the given path, of the given type. @@ -214,21 +209,27 @@ public sealed class DataManager : IDisposable, IServiceType /// /// The path inside of the game files. /// True if the file exists. - public bool FileExists(string path) - { - return this.GameData.FileExists(path); - } + public bool FileExists(string path) + => this.GameData.FileExists(path); /// /// Get a containing the icon with the given ID. /// /// The icon ID. /// The containing the icon. - public TexFile? GetIcon(uint iconId) - { - return this.GetIcon(this.Language, iconId); - } + /// todo: remove in api9 in favor of GetIcon(uint iconId, bool highResolution) + public TexFile? GetIcon(uint iconId) + => this.GetIcon(this.Language, iconId, false); + /// + /// Get a containing the icon with the given ID. + /// + /// The icon ID. + /// Return high resolution version. + /// The containing the icon. + public TexFile? GetIcon(uint iconId, bool highResolution) + => this.GetIcon(this.Language, iconId, highResolution); + /// /// Get a containing the icon with the given ID, of the given quality. /// @@ -247,7 +248,18 @@ public sealed class DataManager : IDisposable, IServiceType /// The requested language. /// The icon ID. /// The containing the icon. + /// todo: remove in api9 in favor of GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution) public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId) + => this.GetIcon(iconLanguage, iconId, false); + + /// + /// Get a containing the icon with the given ID, of the given language. + /// + /// The requested language. + /// The icon ID. + /// Return high resolution version. + /// The containing the icon. + public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution) { var type = iconLanguage switch { @@ -258,7 +270,7 @@ public sealed class DataManager : IDisposable, IServiceType _ => throw new ArgumentOutOfRangeException(nameof(iconLanguage), $"Unknown Language: {iconLanguage}"), }; - return this.GetIcon(type, iconId); + return this.GetIcon(type, iconId, highResolution); } /// @@ -267,20 +279,33 @@ public sealed class DataManager : IDisposable, IServiceType /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon). /// The icon ID. /// The containing the icon. + /// todo: remove in api9 in favor of GetIcon(string? type, uint iconId, bool highResolution) public TexFile? GetIcon(string? type, uint iconId) + => this.GetIcon(type, iconId, false); + + /// + /// Get a containing the icon with the given ID, of the given type. + /// + /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon). + /// The icon ID. + /// Return high resolution version. + /// The containing the icon. + public TexFile? GetIcon(string? type, uint iconId, bool highResolution) { + var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat; + type ??= string.Empty; if (type.Length > 0 && !type.EndsWith("/")) type += "/"; - var filePath = string.Format(IconFileFormat, iconId / 1000, type, iconId); + var filePath = string.Format(format, iconId / 1000, type, iconId); var file = this.GetFile(filePath); if (type == string.Empty || file != default) return file; // Couldn't get specific type, try for generic version. - filePath = string.Format(IconFileFormat, iconId / 1000, string.Empty, iconId); + filePath = string.Format(format, iconId / 1000, string.Empty, iconId); file = this.GetFile(filePath); return file; } @@ -299,9 +324,7 @@ public sealed class DataManager : IDisposable, IServiceType /// The Lumina . /// A that can be used to draw the texture. public TextureWrap? GetImGuiTexture(TexFile? tex) - { - return tex == null ? null : Service.Get().LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4); - } + => tex == null ? null : Service.Get().LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4); /// /// Get the passed texture path as a drawable ImGui TextureWrap. @@ -316,8 +339,18 @@ public sealed class DataManager : IDisposable, IServiceType /// /// The icon ID. /// The containing the icon. - public TextureWrap? GetImGuiTextureIcon(uint iconId) - => this.GetImGuiTexture(this.GetIcon(iconId)); + /// todo: remove in api9 in favor of GetImGuiTextureIcon(uint iconId, bool highResolution) + public TextureWrap? GetImGuiTextureIcon(uint iconId) + => this.GetImGuiTexture(this.GetIcon(iconId, false)); + + /// + /// Get a containing the icon with the given ID. + /// + /// The icon ID. + /// Return the high resolution version. + /// The containing the icon. + public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution) + => this.GetImGuiTexture(this.GetIcon(iconId, highResolution)); /// /// Get a containing the icon with the given ID, of the given quality. From 6ee86156d98823fbad1117b86818c6e2ec983823 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 24 Jun 2023 08:41:00 +0200 Subject: [PATCH 27/28] build: 7.7.4.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 9629a7ec0..61e3d4233 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.7.3.0 + 7.7.4.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 9ebfbb78714103a144e7afd2c50cea23d19530de Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 24 Jun 2023 18:26:32 +0200 Subject: [PATCH 28/28] Update ClientStructs (#1256) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 39b39c12a..f2abb4a11 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 39b39c12a62f17925a38863ccdc54fb43809f915 +Subproject commit f2abb4a11319b26b77cd29b69a52b34e1d56069d