diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index c4c553a47..feed79772 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -115,18 +115,18 @@ 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); + 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) @@ -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/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. diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 33e09e221..19b4f841c 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; @@ -87,59 +88,32 @@ 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"); -#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); - } + var logPath = new FileInfo(Path.Combine(baseDirectory, $"{logFileName}.log")); + var oldPath = new FileInfo(Path.Combine(baseDirectory, $"{logFileName}.old.log")); - CullLogFile(logPath, 1 * 1024 * 1024, oldPath, 10 * 1024 * 1024); + Log.CloseAndFlush(); + + 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))); @@ -159,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 @@ -167,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. @@ -265,86 +239,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/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..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; @@ -642,7 +643,7 @@ internal class DalamudInterface : IDisposable, IServiceType configuration.QueueSave(); EntryPoint.InitLogging( - startInfo.WorkingDirectory!, + startInfo.LogPath!, startInfo.BootShowConsole, configuration.LogSynchronously, startInfo.LogName); @@ -900,7 +901,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/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/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 33b80cf93..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,30 +1109,41 @@ internal class PluginInstallerWindow : Window, IDisposable var useContentWidth = ImGui.GetContentRegionAvail().X; - if (ImGui.BeginChild("InstallerCategories", new Vector2(useContentWidth, useContentHeight * ImGuiHelpers.GlobalScale))) + using var installerMainChild = ImRaii.Child("InstallerCategories", new Vector2(useContentWidth, useContentHeight * ImGuiHelpers.GlobalScale)); + if (installerMainChild) { - 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)); + + try { - 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)) + using (var categoriesChild = ImRaii.Child("InstallerCategoriesSelector", new Vector2(useMenuWidth * ImGuiHelpers.GlobalScale, -1), false)) { - this.DrawPluginCategoryContent(); + if (categoriesChild) + { + this.DrawPluginCategorySelectors(); + } } - ImGui.EndChild(); - ImGui.EndTable(); - } + ImGui.SameLine(); - ImGui.PopStyleVar(); - ImGui.EndChild(); + using var scrollingChild = + ImRaii.Child("ScrollingPlugins", new Vector2(-1, -1), false, ImGuiWindowFlags.NoBackground); + if (scrollingChild) + { + 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"); + } } } @@ -2174,7 +2184,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()) @@ -2350,7 +2363,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)) @@ -2364,12 +2377,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); } } @@ -2388,14 +2401,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(); @@ -2445,7 +2461,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); @@ -2463,7 +2481,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( @@ -2506,7 +2526,7 @@ internal class PluginInstallerWindow : Window, IDisposable } } - StyleModelV1.DalamudStandard.Pop(); + //StyleModelV1.DalamudStandard.Pop(); ImGui.SameLine(); ImGuiHelpers.ScaledDummy(15, 0); @@ -2898,6 +2918,7 @@ internal class PluginInstallerWindow : Window, IDisposable private void OnInstalledPluginsChanged() { var pluginManager = Service.Get(); + using var pmLock = pluginManager.GetSyncScope(); lock (this.listLock) { @@ -3278,6 +3299,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..b43d70e7d 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -107,10 +107,12 @@ internal class ProfileManagerWidget var windowSize = ImGui.GetWindowSize(); - if (ImGui.BeginChild("###profileChooserScrolling")) + using var profileChooserChild = ImRaii.Child("###profileChooserScrolling"); + if (profileChooserChild) { Guid? toCloneGuid = null; + using var syncScope = profman.GetSyncScope(); foreach (var profile in profman.Profiles) { if (profile.IsDefaultProfile) @@ -119,7 +121,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); } @@ -179,8 +181,6 @@ internal class ProfileManagerWidget ImGuiHelpers.CenteredText(Locs.AddProfileHint); ImGui.PopStyleColor(); } - - ImGui.EndChild(); } } @@ -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) @@ -228,9 +229,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 +272,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 +303,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); } @@ -322,12 +325,14 @@ 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; - 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); @@ -391,7 +396,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 +416,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) @@ -436,14 +440,12 @@ internal class ProfileManagerWidget ImGui.TextUnformatted(addPluginsText); ImGuiHelpers.ScaledDummy(10); - - ImGui.EndChild(); } if (wantPluginAddPopup) { this.pickerSearch = string.Empty; - ImGui.OpenPopup(addPluginToProfilePopup); + ImGui.OpenPopup(addPluginToProfilePopupId); } } 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/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"); + } + } +} diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 2d6dbeecd..94bb9467a 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; @@ -69,6 +68,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; @@ -146,19 +149,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. + /// Gets a copy of the 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. + /// Gets a copy of the 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. @@ -173,7 +203,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. @@ -231,6 +261,13 @@ internal partial class PluginManager : IDisposable, IServiceType return false; } + /// + /// 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); + /// /// Print to chat any plugin updates and whether they were successful. /// @@ -309,7 +346,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. @@ -582,7 +619,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,15 +633,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; + } } /// @@ -613,15 +662,18 @@ 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(); - - if (notify) + lock (this.pluginListLock) { - this.NotifyAvailablePluginsChanged(); + this.availablePluginsList.Clear(); + this.availablePluginsList.AddRange(this.Repos + .SelectMany(repo => repo.PluginMaster) + .Where(this.IsManifestEligible) + .Where(IsManifestVisible)); + + if (notify) + { + this.NotifyAvailablePluginsChanged(); + } } } @@ -639,6 +691,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)) { @@ -657,7 +711,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; } @@ -683,7 +737,7 @@ internal partial class PluginManager : IDisposable, IServiceType } if (listChanged) - this.NotifyInstalledPluginsChanged(); + this.NotifyinstalledPluginsListChanged(); } /// @@ -801,7 +855,7 @@ internal partial class PluginManager : IDisposable, IServiceType var plugin = await this.LoadPluginAsync(dllFile, manifest, reason); - this.NotifyInstalledPluginsChanged(); + this.NotifyinstalledPluginsListChanged(); return plugin; } @@ -816,12 +870,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(); } @@ -921,31 +975,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)); @@ -962,7 +1019,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; @@ -1009,7 +1066,7 @@ internal partial class PluginManager : IDisposable, IServiceType lock (this.pluginListLock) { - this.InstalledPlugins = this.InstalledPlugins.Remove(plugin); + this.installedPluginsList.Remove(plugin); } } catch (Exception ex) @@ -1036,7 +1093,7 @@ internal partial class PluginManager : IDisposable, IServiceType } if (notify && updateStatus.WasUpdated) - this.NotifyInstalledPluginsChanged(); + this.NotifyinstalledPluginsListChanged(); return updateStatus; } @@ -1168,7 +1225,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) @@ -1203,11 +1260,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"); } @@ -1215,18 +1272,44 @@ 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) + if (wantsInDefaultProfile == null) { - this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, true, false); + // 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. + } + 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); + 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); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, true, false); + loadPlugin = !doNotLoad; } else if (wantsInDefaultProfile == true && !devPlugin.StartOnBoot) { - this.profileManager.DefaultProfile.AddOrUpdate(probablyInternalNameForThisPurpose, false, false); + // 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); + 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); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false); + loadPlugin = false; } plugin = devPlugin; @@ -1242,7 +1325,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) { @@ -1311,9 +1394,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; @@ -1321,39 +1407,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() @@ -1363,7 +1450,7 @@ internal partial class PluginManager : IDisposable, IServiceType this.OnAvailablePluginsChanged?.InvokeSafely(); } - private void NotifyInstalledPluginsChanged() + private void NotifyinstalledPluginsListChanged() { this.DetectAvailablePluginUpdates(); @@ -1372,7 +1459,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; diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index bae3e6f95..71feff0c2 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; @@ -108,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. @@ -115,7 +123,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 +136,7 @@ internal class Profile Service.Get().QueueSave(); if (apply) - this.manager.ApplyAllWantStates(); + await this.manager.ApplyAllWantStatesAsync(); } /// @@ -137,8 +146,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) { - 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; + } } /// @@ -148,34 +160,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) { 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(); } /// @@ -184,21 +200,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) { - 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 { @@ -209,6 +230,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 d91db1283..46b572c1a 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; @@ -41,7 +40,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. @@ -52,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. @@ -60,25 +73,29 @@ 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; - - foreach (var profile in this.profiles) + + lock (this.profiles) { - var state = profile.WantsPlugin(internalName); - if (state.HasValue) + 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); + await this.DefaultProfile.AddOrUpdateAsync(internalName, defaultState, false); + return defaultState; } @@ -91,7 +108,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. @@ -167,16 +187,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() { + 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) { @@ -185,9 +213,9 @@ internal class ProfileManager : IServiceType Log.Information("Applying want states..."); - var pm = Service.Get(); var tasks = new List(); + var pm = Service.Get(); foreach (var installedPlugin in pm.InstalledPlugins) { var wantThis = wantActive.Contains(installedPlugin.Manifest.InternalName); @@ -215,7 +243,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) { @@ -233,12 +261,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 062fc94f1..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. @@ -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. 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); + } +} diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index c1147393f..bf816cbc8 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 the assembly version of Dalamud. /// @@ -650,6 +651,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) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 130a4df52..f2abb4a11 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 130a4df5298e9bcdf343ce1d93ee0afdafac587f +Subproject commit f2abb4a11319b26b77cd29b69a52b34e1d56069d