diff --git a/Dalamud.Boot/xivfixes.cpp b/Dalamud.Boot/xivfixes.cpp index f3b6aaa2c..eb0f7df56 100644 --- a/Dalamud.Boot/xivfixes.cpp +++ b/Dalamud.Boot/xivfixes.cpp @@ -648,6 +648,48 @@ void xivfixes::symbol_load_patches(bool bApply) { } } +void xivfixes::disable_game_debugging_protection(bool bApply) { + static const char* LogTag = "[xivfixes:disable_game_debugging_protection]"; + static const std::vector patchBytes = { + 0x31, 0xC0, // XOR EAX, EAX + 0x90, // NOP + 0x90, // NOP + 0x90, // NOP + 0x90 // NOP + }; + + if (!bApply) + return; + + if (!g_startInfo.BootEnabledGameFixes.contains("disable_game_debugging_protection")) { + logging::I("{} Turned off via environment variable.", LogTag); + return; + } + + // Find IsDebuggerPresent in Framework.Tick() + const char* matchPtr = utils::signature_finder() + .look_in(utils::loaded_module(g_hGameInstance), ".text") + .look_for_hex("FF 15 ?? ?? ?? ?? 85 C0 74 13 41") + .find_one() + .Match.data(); + + if (!matchPtr) { + logging::E("{} Failed to find signature.", LogTag); + return; + } + + void* address = const_cast(static_cast(matchPtr)); + + DWORD oldProtect; + if (VirtualProtect(address, patchBytes.size(), PAGE_EXECUTE_READWRITE, &oldProtect)) { + memcpy(address, patchBytes.data(), patchBytes.size()); + VirtualProtect(address, patchBytes.size(), oldProtect, &oldProtect); + logging::I("{} Patch applied at address 0x{:X}.", LogTag, reinterpret_cast(address)); + } else { + logging::E("{} Failed to change memory protection.", LogTag); + } +} + void xivfixes::apply_all(bool bApply) { for (const auto& [taskName, taskFunction] : std::initializer_list> { @@ -658,6 +700,7 @@ void xivfixes::apply_all(bool bApply) { { "backup_userdata_save", &backup_userdata_save }, { "prevent_icmphandle_crashes", &prevent_icmphandle_crashes }, { "symbol_load_patches", &symbol_load_patches }, + { "disable_game_debugging_protection", &disable_game_debugging_protection }, } ) { try { diff --git a/Dalamud.Boot/xivfixes.h b/Dalamud.Boot/xivfixes.h index afe2edb45..1cab3afae 100644 --- a/Dalamud.Boot/xivfixes.h +++ b/Dalamud.Boot/xivfixes.h @@ -8,6 +8,7 @@ namespace xivfixes { void backup_userdata_save(bool bApply); void prevent_icmphandle_crashes(bool bApply); void symbol_load_patches(bool bApply); + void disable_game_debugging_protection(bool bApply); void apply_all(bool bApply); } diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index 951050b33..1942e271b 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -46,6 +46,8 @@ namespace Dalamud.CorePlugin #else private readonly WindowSystem windowSystem = new("Dalamud.CorePlugin"); + private readonly PluginWindow window; + private Localization localization; private IPluginLog pluginLog; @@ -63,7 +65,8 @@ namespace Dalamud.CorePlugin this.Interface = pluginInterface; this.pluginLog = log; - this.windowSystem.AddWindow(new PluginWindow()); + this.window = new PluginWindow(); + this.windowSystem.AddWindow(this.window); this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; @@ -136,12 +139,12 @@ namespace Dalamud.CorePlugin { this.pluginLog.Information("Command called!"); - // this.window.IsOpen = true; + this.window.IsOpen ^= true; } private void OnOpenConfigUi() { - // this.window.IsOpen = true; + this.window.IsOpen = true; } private void OnOpenMainUi() diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 97cb1fd9e..4f876102e 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -478,6 +478,7 @@ namespace Dalamud.Injector "backup_userdata_save", "prevent_icmphandle_crashes", "symbol_load_patches", + "disable_game_debugging_protection", }; startInfo.BootDotnetOpenProcessHookMode = 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0; diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 24e2a4266..69961a69b 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -3,12 +3,14 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; +using System.Numerics; using System.Runtime.InteropServices; using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface; using Dalamud.Interface.FontIdentifier; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Interface.Style; @@ -67,12 +69,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public List? BadWords { get; set; } /// - /// Gets or sets a value indicating whether or not the taskbar should flash once a duty is found. + /// Gets or sets a value indicating whether the taskbar should flash once a duty is found. /// public bool DutyFinderTaskbarFlash { get; set; } = true; /// - /// Gets or sets a value indicating whether or not a message should be sent in chat once a duty is found. + /// Gets or sets a value indicating whether a message should be sent in chat once a duty is found. /// public bool DutyFinderChatMessage { get; set; } = true; @@ -102,7 +104,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public XivChatType GeneralChatType { get; set; } = XivChatType.Debug; /// - /// Gets or sets a value indicating whether or not plugin testing builds should be shown. + /// Gets or sets a value indicating whether plugin testing builds should be shown. /// public bool DoPluginTest { get; set; } = false; @@ -117,7 +119,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public List ThirdRepoList { get; set; } = new(); /// - /// Gets or sets a value indicating whether or not a disclaimer regarding third-party repos has been dismissed. + /// Gets or sets a value indicating whether a disclaimer regarding third-party repos has been dismissed. /// public bool? ThirdRepoSpeedbumpDismissed { get; set; } = null; @@ -175,38 +177,38 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public float ImeStateIndicatorOpacity { get; set; } = 1f; /// - /// Gets or sets a value indicating whether or not plugin UI should be hidden. + /// Gets or sets a value indicating whether plugin UI should be hidden. /// public bool ToggleUiHide { get; set; } = true; /// - /// Gets or sets a value indicating whether or not plugin UI should be hidden during cutscenes. + /// Gets or sets a value indicating whether plugin UI should be hidden during cutscenes. /// public bool ToggleUiHideDuringCutscenes { get; set; } = true; /// - /// Gets or sets a value indicating whether or not plugin UI should be hidden during GPose. + /// Gets or sets a value indicating whether plugin UI should be hidden during GPose. /// public bool ToggleUiHideDuringGpose { get; set; } = true; /// - /// Gets or sets a value indicating whether or not a message containing Dalamud's current version and the number of loaded plugins should be sent at login. + /// Gets or sets a value indicating whether a message containing Dalamud's current version and the number of loaded plugins should be sent at login. /// public bool PrintDalamudWelcomeMsg { get; set; } = true; /// - /// Gets or sets a value indicating whether or not a message containing detailed plugin information should be sent at login. + /// Gets or sets a value indicating whether a message containing detailed plugin information should be sent at login. /// public bool PrintPluginsWelcomeMsg { get; set; } = true; /// - /// Gets or sets a value indicating whether or not plugins should be auto-updated. + /// Gets or sets a value indicating whether plugins should be auto-updated. /// [Obsolete("Use AutoUpdateBehavior instead.")] public bool AutoUpdatePlugins { get; set; } /// - /// Gets or sets a value indicating whether or not Dalamud should add buttons to the system menu. + /// Gets or sets a value indicating whether Dalamud should add buttons to the system menu. /// public bool DoButtonsSystemMenu { get; set; } = true; @@ -221,12 +223,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public bool LogSynchronously { get; set; } = false; /// - /// Gets or sets a value indicating whether or not the debug log should scroll automatically. + /// Gets or sets a value indicating whether the debug log should scroll automatically. /// public bool LogAutoScroll { get; set; } = true; /// - /// Gets or sets a value indicating whether or not the debug log should open at startup. + /// Gets or sets a value indicating whether the debug log should open at startup. /// public bool LogOpenAtStartup { get; set; } @@ -241,29 +243,29 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public List LogCommandHistory { get; set; } = new(); /// - /// Gets or sets a value indicating whether or not the dev bar should open at startup. + /// Gets or sets a value indicating whether the dev bar should open at startup. /// public bool DevBarOpenAtStartup { get; set; } /// - /// Gets or sets a value indicating whether or not ImGui asserts should be enabled at startup. + /// Gets or sets a value indicating whether ImGui asserts should be enabled at startup. /// public bool? ImGuiAssertsEnabledAtStartup { get; set; } /// - /// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui. + /// Gets or sets a value indicating whether docking should be globally enabled in ImGui. /// public bool IsDocking { get; set; } /// - /// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects. + /// Gets or sets a value indicating whether plugin user interfaces should trigger sound effects. /// This setting is effected by the in-game "System Sounds" option and volume. /// [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")] public bool EnablePluginUISoundEffects { get; set; } /// - /// Gets or sets a value indicating whether or not an additional button allowing pinning and clickthrough options should be shown + /// Gets or sets a value indicating whether an additional button allowing pinning and clickthrough options should be shown /// on plugin title bars when using the Window System. /// public bool EnablePluginUiAdditionalOptions { get; set; } = true; @@ -274,20 +276,15 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public bool IsDisableViewport { get; set; } = true; /// - /// Gets or sets a value indicating whether or not navigation via a gamepad should be globally enabled in ImGui. + /// Gets or sets a value indicating whether navigation via a gamepad should be globally enabled in ImGui. /// public bool IsGamepadNavigationEnabled { get; set; } = true; /// - /// Gets or sets a value indicating whether or not focus management is enabled. + /// Gets or sets a value indicating whether focus management is enabled. /// public bool IsFocusManagementEnabled { get; set; } = true; - /// - /// Gets or sets a value indicating whether or not the anti-anti-debug check is enabled on startup. - /// - public bool IsAntiAntiDebugEnabled { get; set; } = false; - /// /// Gets or sets a value indicating whether to resume game main thread after plugins load. /// @@ -299,7 +296,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public string? DalamudBetaKind { get; set; } /// - /// Gets or sets a value indicating whether or not any plugin should be loaded when the game is started. + /// Gets or sets a value indicating whether any plugin should be loaded when the game is started. /// It is reset immediately when read. /// public bool PluginSafeMode { get; set; } @@ -311,7 +308,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public int? PluginWaitBeforeFree { get; set; } /// - /// Gets or sets a value indicating whether or not crashes during shutdown should be reported. + /// Gets or sets a value indicating whether crashes during shutdown should be reported. /// public bool ReportShutdownCrashes { get; set; } @@ -343,12 +340,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public ProfileModel? DefaultProfile { get; set; } /// - /// Gets or sets a value indicating whether or not profiles are enabled. + /// Gets or sets a value indicating whether profiles are enabled. /// public bool ProfilesEnabled { get; set; } = false; /// - /// Gets or sets a value indicating whether or not the user has seen the profiles tutorial. + /// Gets or sets a value indicating whether the user has seen the profiles tutorial. /// public bool ProfilesHasSeenTutorial { get; set; } = false; @@ -392,7 +389,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public bool? ReduceMotions { get; set; } /// - /// Gets or sets a value indicating whether or not market board data should be uploaded. + /// Gets or sets a value indicating whether market board data should be uploaded. /// public bool IsMbCollect { get; set; } = true; @@ -428,7 +425,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService } /// - /// Gets or sets a value indicating whether or not to show info on dev bar. + /// Gets or sets a value indicating whether to show info on dev bar. /// public bool ShowDevBarInfo { get; set; } = true; @@ -502,6 +499,16 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// public bool SendUpdateNotificationToChat { get; set; } = false; + /// + /// Gets or sets a value indicating whether disabled plugins should be auto-updated. + /// + public bool UpdateDisabledPlugins { get; set; } = false; + + /// + /// Gets or sets a value indicating where notifications are anchored to on the screen. + /// + public Vector2 NotificationAnchorPosition { get; set; } = new(1f, 1f); + /// /// Load a configuration from the provided path. /// @@ -562,6 +569,8 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public void ForceSave() { this.Save(); + this.isSaveQueued = false; + this.writeTask?.GetAwaiter().GetResult(); } /// diff --git a/Dalamud/Configuration/Internal/DevPluginSettings.cs b/Dalamud/Configuration/Internal/DevPluginSettings.cs index 361632a14..64327e658 100644 --- a/Dalamud/Configuration/Internal/DevPluginSettings.cs +++ b/Dalamud/Configuration/Internal/DevPluginSettings.cs @@ -12,16 +12,22 @@ internal sealed class DevPluginSettings /// public bool StartOnBoot { get; set; } = true; + /// + /// Gets or sets a value indicating whether we should show notifications for errors this plugin + /// is creating. + /// + public bool NotifyForErrors { get; set; } = true; + /// /// Gets or sets a value indicating whether this plugin should automatically reload on file change. /// public bool AutomaticReloading { get; set; } = false; - + /// /// Gets or sets an ID uniquely identifying this specific instance of a devPlugin. /// public Guid WorkingPluginId { get; set; } = Guid.Empty; - + /// /// Gets or sets a list of validation problems that have been dismissed by the user. /// diff --git a/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs b/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs index 11a8d3567..4b7e6dd3d 100644 --- a/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs +++ b/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs @@ -21,7 +21,7 @@ internal class EnvironmentConfiguration public static bool DalamudForceMinHook { get; } = GetEnvironmentVariable("DALAMUD_FORCE_MINHOOK"); /// - /// Gets a value indicating whether or not Dalamud context menus should be disabled. + /// Gets a value indicating whether Dalamud context menus should be disabled. /// public static bool DalamudDoContextMenu { get; } = GetEnvironmentVariable("DALAMUD_ENABLE_CONTEXTMENU"); diff --git a/Dalamud/Console/ConsoleEntry.cs b/Dalamud/Console/ConsoleEntry.cs index 93f250228..407411c6b 100644 --- a/Dalamud/Console/ConsoleEntry.cs +++ b/Dalamud/Console/ConsoleEntry.cs @@ -11,13 +11,13 @@ public interface IConsoleEntry /// Gets the name of the entry. /// public string Name { get; } - + /// /// Gets the description of the entry. /// public string Description { get; } } - + /// /// Interface representing a command in the console. /// @@ -27,7 +27,7 @@ public interface IConsoleCommand : IConsoleEntry /// Execute this command. /// /// Arguments to invoke the entry with. - /// Whether or not execution succeeded. + /// Whether execution succeeded. public bool Invoke(IEnumerable arguments); } diff --git a/Dalamud/Console/ConsoleManager.cs b/Dalamud/Console/ConsoleManager.cs index 4112cde2a..c79a104e1 100644 --- a/Dalamud/Console/ConsoleManager.cs +++ b/Dalamud/Console/ConsoleManager.cs @@ -18,9 +18,9 @@ namespace Dalamud.Console; internal partial class ConsoleManager : IServiceType { private static readonly ModuleLog Log = new("CON"); - + private Dictionary entries = new(); - + /// /// Initializes a new instance of the class. /// @@ -29,17 +29,17 @@ internal partial class ConsoleManager : IServiceType { this.AddCommand("toggle", "Toggle a boolean variable.", this.OnToggleVariable); } - + /// /// Event that is triggered when a command is processed. Return true to stop the command from being processed any further. /// public event Func? Invoke; - + /// /// Gets a read-only dictionary of console entries. /// public IReadOnlyDictionary Entries => this.entries; - + /// /// Add a command to the console. /// @@ -53,13 +53,13 @@ internal partial class ConsoleManager : IServiceType ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(description); ArgumentNullException.ThrowIfNull(func); - + if (this.FindEntry(name) != null) throw new InvalidOperationException($"Entry '{name}' already exists."); var command = new ConsoleCommand(name, description, func); this.entries.Add(name, command); - + return command; } @@ -77,14 +77,14 @@ internal partial class ConsoleManager : IServiceType ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(description); Traits.ThrowIfTIsNullableAndNull(defaultValue); - + if (this.FindEntry(name) != null) throw new InvalidOperationException($"Entry '{name}' already exists."); var variable = new ConsoleVariable(name, description); variable.Value = defaultValue; this.entries.Add(name, variable); - + return variable; } @@ -98,11 +98,11 @@ internal partial class ConsoleManager : IServiceType { ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(alias); - + var target = this.FindEntry(name); if (target == null) throw new EntryNotFoundException(name); - + if (this.FindEntry(alias) != null) throw new InvalidOperationException($"Entry '{alias}' already exists."); @@ -135,21 +135,21 @@ internal partial class ConsoleManager : IServiceType public T GetVariable(string name) { ArgumentNullException.ThrowIfNull(name); - + var entry = this.FindEntry(name); - + if (entry is ConsoleVariable variable) return variable.Value; - + if (entry is ConsoleVariable) throw new InvalidOperationException($"Variable '{name}' is not of type {typeof(T).Name}."); - + if (entry is null) throw new EntryNotFoundException(name); - + throw new InvalidOperationException($"Command '{name}' is not a variable."); } - + /// /// Set the value of a variable. /// @@ -162,18 +162,18 @@ internal partial class ConsoleManager : IServiceType { ArgumentNullException.ThrowIfNull(name); Traits.ThrowIfTIsNullableAndNull(value); - + var entry = this.FindEntry(name); - + if (entry is ConsoleVariable variable) variable.Value = value; - + if (entry is ConsoleVariable) throw new InvalidOperationException($"Variable '{name}' is not of type {typeof(T).Name}."); if (entry is null) - throw new EntryNotFoundException(name); - + throw new EntryNotFoundException(name); + throw new InvalidOperationException($"Command '{name}' is not a variable."); } @@ -181,16 +181,16 @@ internal partial class ConsoleManager : IServiceType /// Process a console command. /// /// The command to process. - /// Whether or not the command was successfully processed. + /// Whether the command was successfully processed. public bool ProcessCommand(string command) { if (this.Invoke?.Invoke(command) == true) return true; - + var matches = GetCommandParsingRegex().Matches(command); if (matches.Count == 0) return false; - + var entryName = matches[0].Value; if (string.IsNullOrEmpty(entryName) || entryName.Any(char.IsWhiteSpace)) { @@ -204,7 +204,7 @@ internal partial class ConsoleManager : IServiceType Log.Error("Command {CommandName} not found", entryName); return false; } - + var parsedArguments = new List(); if (entry.ValidArguments != null) @@ -217,13 +217,13 @@ internal partial class ConsoleManager : IServiceType PrintUsage(entry); return false; } - + var argumentToMatch = entry.ValidArguments[i - 1]; - + var group = matches[i]; if (!group.Success) continue; - + var value = group.Value; if (string.IsNullOrEmpty(value)) continue; @@ -262,15 +262,15 @@ internal partial class ConsoleManager : IServiceType throw new Exception("Unhandled argument type."); } } - + if (parsedArguments.Count != entry.ValidArguments.Count) { // Either fill in the default values or error out - + for (var i = parsedArguments.Count; i < entry.ValidArguments.Count; i++) { var argument = entry.ValidArguments[i]; - + // If the default value is DBNull, we need to error out as that means it was not specified if (argument.DefaultValue == DBNull.Value) { @@ -281,7 +281,7 @@ internal partial class ConsoleManager : IServiceType parsedArguments.Add(argument.DefaultValue); } - + if (parsedArguments.Count != entry.ValidArguments.Count) { Log.Error("Too many arguments for command {CommandName}", entryName); @@ -302,20 +302,20 @@ internal partial class ConsoleManager : IServiceType return entry.Invoke(parsedArguments); } - + [GeneratedRegex("""("[^"]+"|[^\s"]+)""", RegexOptions.Compiled)] private static partial Regex GetCommandParsingRegex(); - + private static void PrintUsage(ConsoleEntry entry, bool error = true) { Log.WriteLog( - error ? LogEventLevel.Error : LogEventLevel.Information, + error ? LogEventLevel.Error : LogEventLevel.Information, "Usage: {CommandName} {Arguments}", null, entry.Name, string.Join(" ", entry.ValidArguments?.Select(x => $"<{x.Type.ToString().ToLowerInvariant()}>") ?? Enumerable.Empty())); } - + private ConsoleEntry? FindEntry(string name) { return this.entries.TryGetValue(name, out var entry) ? entry as ConsoleEntry : null; @@ -333,7 +333,7 @@ internal partial class ConsoleManager : IServiceType return true; } - + private static class Traits { public static void ThrowIfTIsNullableAndNull(T? argument, [CallerArgumentExpression("argument")] string? paramName = null) @@ -364,17 +364,17 @@ internal partial class ConsoleManager : IServiceType /// public string Description { get; } - + /// /// Gets or sets a list of valid argument types for this console entry. /// public IReadOnlyList? ValidArguments { get; protected set; } - + /// /// Execute this command. /// /// Arguments to invoke the entry with. - /// Whether or not execution succeeded. + /// Whether execution succeeded. public abstract bool Invoke(IEnumerable arguments); /// @@ -388,19 +388,19 @@ internal partial class ConsoleManager : IServiceType { if (type == typeof(string)) return new ArgumentInfo(ConsoleArgumentType.String, defaultValue); - + if (type == typeof(int)) return new ArgumentInfo(ConsoleArgumentType.Integer, defaultValue); - + if (type == typeof(float)) return new ArgumentInfo(ConsoleArgumentType.Float, defaultValue); - + if (type == typeof(bool)) return new ArgumentInfo(ConsoleArgumentType.Bool, defaultValue); - + throw new ArgumentException($"Invalid argument type: {type.Name}"); } - + public record ArgumentInfo(ConsoleArgumentType Type, object? DefaultValue); } @@ -436,7 +436,7 @@ internal partial class ConsoleManager : IServiceType private class ConsoleCommand : ConsoleEntry, IConsoleCommand { private readonly Delegate func; - + /// /// Initializes a new instance of the class. /// @@ -447,17 +447,17 @@ internal partial class ConsoleManager : IServiceType : base(name, description) { this.func = func; - + if (func.Method.ReturnType != typeof(bool)) throw new ArgumentException("Console command functions must return a boolean indicating success."); - + var validArguments = new List(); foreach (var parameterInfo in func.Method.GetParameters()) { var paraT = parameterInfo.ParameterType; validArguments.Add(TypeToArgument(paraT, parameterInfo.DefaultValue)); } - + this.ValidArguments = validArguments; } @@ -491,7 +491,7 @@ internal partial class ConsoleManager : IServiceType { this.ValidArguments = new List { TypeToArgument(typeof(T), null) }; } - + /// public T Value { get; set; } @@ -507,16 +507,16 @@ internal partial class ConsoleManager : IServiceType { this.Value = (T)(object)!boolValue; } - + Log.WriteLog(LogEventLevel.Information, "{VariableName} = {VariableValue}", null, this.Name, this.Value); return true; } - + if (first.GetType() != typeof(T)) throw new ArgumentException($"Console variable must be set with an argument of type {typeof(T).Name}."); this.Value = (T)first; - + return true; } } diff --git a/Dalamud/Console/ConsoleManagerPluginScoped.cs b/Dalamud/Console/ConsoleManagerPluginScoped.cs index e1eddcf7a..41949c7d7 100644 --- a/Dalamud/Console/ConsoleManagerPluginScoped.cs +++ b/Dalamud/Console/ConsoleManagerPluginScoped.cs @@ -11,6 +11,47 @@ namespace Dalamud.Console; #pragma warning disable Dalamud001 +/// +/// Utility functions for the console manager. +/// +internal static partial class ConsoleManagerPluginUtil +{ + private static readonly string[] ReservedNamespaces = ["dalamud", "xl", "plugin"]; + + /// + /// Get a sanitized namespace name from a plugin's internal name. + /// + /// The plugin's internal name. + /// A sanitized namespace. + public static string GetSanitizedNamespaceName(string pluginInternalName) + { + // Must be lowercase + pluginInternalName = pluginInternalName.ToLowerInvariant(); + + // Remove all non-alphabetic characters + pluginInternalName = NonAlphaRegex().Replace(pluginInternalName, string.Empty); + + // Remove reserved namespaces from the start or end + foreach (var reservedNamespace in ReservedNamespaces) + { + if (pluginInternalName.StartsWith(reservedNamespace)) + { + pluginInternalName = pluginInternalName[reservedNamespace.Length..]; + } + + if (pluginInternalName.EndsWith(reservedNamespace)) + { + pluginInternalName = pluginInternalName[..^reservedNamespace.Length]; + } + } + + return pluginInternalName; + } + + [GeneratedRegex(@"[^a-z]")] + private static partial Regex NonAlphaRegex(); +} + /// /// Plugin-scoped version of the console service. /// @@ -19,11 +60,11 @@ namespace Dalamud.Console; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService +internal class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService { [ServiceManager.ServiceDependency] private readonly ConsoleManager console = Service.Get(); - + private readonly List trackedEntries = new(); /// @@ -38,7 +79,7 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService /// public string Prefix { get; private set; } - + /// void IInternalDisposableService.DisposeService() { @@ -46,7 +87,7 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService { this.console.RemoveEntry(trackedEntry); } - + this.trackedEntries.Clear(); } @@ -108,21 +149,21 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService this.console.RemoveEntry(entry); this.trackedEntries.Remove(entry); } - + private string GetPrefixedName(string name) { ArgumentNullException.ThrowIfNull(name); - + // If the name is empty, return the prefix to allow for a single command or variable to be top-level. if (name.Length == 0) return this.Prefix; - + if (name.Any(char.IsWhiteSpace)) throw new ArgumentException("Name cannot contain whitespace.", nameof(name)); - + return $"{this.Prefix}.{name}"; } - + private IConsoleCommand InternalAddCommand(string name, string description, Delegate func) { var command = this.console.AddCommand(this.GetPrefixedName(name), description, func); @@ -130,44 +171,3 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService return command; } } - -/// -/// Utility functions for the console manager. -/// -internal static partial class ConsoleManagerPluginUtil -{ - private static readonly string[] ReservedNamespaces = ["dalamud", "xl", "plugin"]; - - /// - /// Get a sanitized namespace name from a plugin's internal name. - /// - /// The plugin's internal name. - /// A sanitized namespace. - public static string GetSanitizedNamespaceName(string pluginInternalName) - { - // Must be lowercase - pluginInternalName = pluginInternalName.ToLowerInvariant(); - - // Remove all non-alphabetic characters - pluginInternalName = NonAlphaRegex().Replace(pluginInternalName, string.Empty); - - // Remove reserved namespaces from the start or end - foreach (var reservedNamespace in ReservedNamespaces) - { - if (pluginInternalName.StartsWith(reservedNamespace)) - { - pluginInternalName = pluginInternalName[reservedNamespace.Length..]; - } - - if (pluginInternalName.EndsWith(reservedNamespace)) - { - pluginInternalName = pluginInternalName[..^reservedNamespace.Length]; - } - } - - return pluginInternalName; - } - - [GeneratedRegex(@"[^a-z]")] - private static partial Regex NonAlphaRegex(); -} diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 03c7463d4..a411883d5 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -91,7 +91,7 @@ internal sealed unsafe class Dalamud : IServiceType return; Util.Fatal( - "Dalamud failed to load all necessary services.\n\nThe game will continue, but you may not be able to use plugins.", + $"Dalamud failed to load all necessary services.\nThe game will continue, but you may not be able to use plugins.\n\n{t.Exception}", "Dalamud", false); } diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index e613c71f5..b13de0b77 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 12.0.0.7 + 12.0.1.4 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) @@ -64,8 +64,8 @@ - - + + @@ -130,6 +130,13 @@ + + + + + + + diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 9e84a60c8..9fc09a56b 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -321,7 +321,7 @@ public sealed class EntryPoint Log.Information("User chose to disable plugins on next launch..."); var config = Service.Get(); config.PluginSafeMode = true; - config.QueueSave(); + config.ForceSave(); } Log.CloseAndFlush(); diff --git a/Dalamud/Game/Addon/Events/AddonEventData.cs b/Dalamud/Game/Addon/Events/AddonEventData.cs new file mode 100644 index 000000000..3a5c05660 --- /dev/null +++ b/Dalamud/Game/Addon/Events/AddonEventData.cs @@ -0,0 +1,46 @@ +namespace Dalamud.Game.Addon.Events; + +/// +/// Object representing data that is relevant in handling native events. +/// +public class AddonEventData +{ + /// + /// Gets the AtkEventType for this event. + /// + public AddonEventType AtkEventType { get; internal set; } + + /// + /// Gets the param field for this event. + /// + public uint Param { get; internal set; } + + /// + /// Gets the pointer to the AtkEvent object for this event. + /// + /// Note: This is not a pointer to the AtkEventData object.

+ /// Warning: AtkEvent->Node has been modified to be the AtkUnitBase*, and AtkEvent->Target has been modified to be the AtkResNode* that triggered this event. + public nint AtkEventPointer { get; internal set; } + + /// + /// Gets the pointer to the AtkEventData object for this event. + /// + /// This field will contain relevant data such as left vs right click, scroll up vs scroll down. + public nint AtkEventDataPointer { get; internal set; } + + /// + /// Gets the pointer to the AtkUnitBase that is handling this event. + /// + public nint AddonPointer { get; internal set; } + + /// + /// Gets the pointer to the AtkResNode that triggered this event. + /// + public nint NodeTargetPointer { get; internal set; } + + /// + /// Gets or sets a pointer to the AtkEventListener responsible for handling this event. + /// Note: As the event listener is dalamud allocated, there's no reason to expose this field. + /// + internal nint AtkEventListener { get; set; } +} diff --git a/Dalamud/Game/Addon/Events/AddonEventEntry.cs b/Dalamud/Game/Addon/Events/AddonEventEntry.cs index 8b5808087..50b9c7ec4 100644 --- a/Dalamud/Game/Addon/Events/AddonEventEntry.cs +++ b/Dalamud/Game/Addon/Events/AddonEventEntry.cs @@ -1,5 +1,6 @@ -using Dalamud.Memory; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.Services; +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Events; @@ -14,9 +15,9 @@ internal unsafe class AddonEventEntry /// Name of an invalid addon. ///
public const string InvalidAddonName = "NullAddon"; - + private string? addonName; - + /// /// Gets the pointer to the addons AtkUnitBase. /// @@ -35,18 +36,25 @@ internal unsafe class AddonEventEntry /// /// Gets the handler that gets called when this event is triggered. /// - public required IAddonEventManager.AddonEventHandler Handler { get; init; } - + [Obsolete("Use AddonEventDelegate Delegate instead")] + public IAddonEventManager.AddonEventHandler Handler { get; init; } + + /// + /// Gets the delegate that gets called when this event is triggered. + /// + [Api13ToDo("Make this field required")] + public IAddonEventManager.AddonEventDelegate Delegate { get; init; } + /// /// Gets the unique id for this event. /// public required uint ParamKey { get; init; } - + /// /// Gets the event type for this event. /// public required AddonEventType EventType { get; init; } - + /// /// Gets the event handle for this event. /// diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index dce2a7e73..0990c1f5f 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -24,21 +24,21 @@ internal unsafe class AddonEventManager : IInternalDisposableService /// PluginName for Dalamud Internal use. ///
public static readonly Guid DalamudInternalKey = Guid.NewGuid(); - + private static readonly ModuleLog Log = new("AddonEventManager"); - + [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycle = Service.Get(); private readonly AddonLifecycleEventListener finalizeEventListener; - + private readonly AddonEventManagerAddressResolver address; private readonly Hook onUpdateCursor; private readonly ConcurrentDictionary pluginEventControllers; - + private AddonCursorType? cursorOverride; - + [ServiceManager.ServiceConstructor] private AddonEventManager(TargetSigScanner sigScanner) { @@ -47,7 +47,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService this.pluginEventControllers = new ConcurrentDictionary(); this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController()); - + this.cursorOverride = null; this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); @@ -69,7 +69,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService { pluginEventController.Dispose(); } - + this.addonLifecycle.UnregisterListener(this.finalizeEventListener); } @@ -92,7 +92,30 @@ internal unsafe class AddonEventManager : IInternalDisposableService { Log.Verbose($"Unable to locate controller for {pluginId}. No event was added."); } - + + return null; + } + + /// + /// Registers an event handler for the specified addon, node, and type. + /// + /// Unique ID for this plugin. + /// The parent addon for this event. + /// The node that will trigger this event. + /// The event type for this event. + /// The delegate to call when event is triggered. + /// IAddonEventHandle used to remove the event. + internal IAddonEventHandle? AddEvent(Guid pluginId, nint atkUnitBase, nint atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventDelegate eventDelegate) + { + if (this.pluginEventControllers.TryGetValue(pluginId, out var controller)) + { + return controller.AddEvent(atkUnitBase, atkResNode, eventType, eventDelegate); + } + else + { + Log.Verbose($"Unable to locate controller for {pluginId}. No event was added."); + } + return null; } @@ -112,7 +135,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService Log.Verbose($"Unable to locate controller for {pluginId}. No event was removed."); } } - + /// /// Force the game cursor to be the specified cursor. /// @@ -167,21 +190,21 @@ internal unsafe class AddonEventManager : IInternalDisposableService pluginList.Value.RemoveForAddon(addonInfo.AddonName); } } - + private nint UpdateCursorDetour(RaptureAtkModule* module) { try { var atkStage = AtkStage.Instance(); - + if (this.cursorOverride is not null && atkStage is not null) { var cursor = (AddonCursorType)atkStage->AtkCursor.Type; - if (cursor != this.cursorOverride) + if (cursor != this.cursorOverride) { AtkStage.Instance()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1); } - + return nint.Zero; } } @@ -218,7 +241,7 @@ internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddo public AddonEventManagerPluginScoped(LocalPlugin plugin) { this.plugin = plugin; - + this.eventManagerService.AddPluginEventController(plugin.EffectiveWorkingPluginId); } @@ -230,31 +253,38 @@ internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddo { this.eventManagerService.ResetCursor(); } - - this.eventManagerService.RemovePluginEventController(this.plugin.EffectiveWorkingPluginId); + + Service.Get().RunOnFrameworkThread(() => + { + this.eventManagerService.RemovePluginEventController(this.plugin.EffectiveWorkingPluginId); + }).Wait(); } - + /// - public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) => this.eventManagerService.AddEvent(this.plugin.EffectiveWorkingPluginId, atkUnitBase, atkResNode, eventType, eventHandler); + /// + public IAddonEventHandle? AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventDelegate eventDelegate) + => this.eventManagerService.AddEvent(this.plugin.EffectiveWorkingPluginId, atkUnitBase, atkResNode, eventType, eventDelegate); + /// public void RemoveEvent(IAddonEventHandle eventHandle) => this.eventManagerService.RemoveEvent(this.plugin.EffectiveWorkingPluginId, eventHandle); - + /// public void SetCursor(AddonCursorType cursor) { this.isForcingCursor = true; - + this.eventManagerService.SetCursor(cursor); } - + /// public void ResetCursor() { this.isForcingCursor = false; - + this.eventManagerService.ResetCursor(); } } diff --git a/Dalamud/Game/Addon/Events/AddonEventType.cs b/Dalamud/Game/Addon/Events/AddonEventType.cs index 100168e22..cd04152ca 100644 --- a/Dalamud/Game/Addon/Events/AddonEventType.cs +++ b/Dalamud/Game/Addon/Events/AddonEventType.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Addon.Events; +namespace Dalamud.Game.Addon.Events; /// /// Reimplementation of AtkEventType. @@ -9,150 +9,262 @@ public enum AddonEventType : byte /// Mouse Down. /// MouseDown = 3, - + /// /// Mouse Up. /// MouseUp = 4, - + /// /// Mouse Move. /// MouseMove = 5, - + /// /// Mouse Over. /// MouseOver = 6, - + /// /// Mouse Out. /// MouseOut = 7, - + + /// + /// Mouse Wheel. + /// + MouseWheel = 8, + /// /// Mouse Click. /// MouseClick = 9, - + + /// + /// Mouse Double Click. + /// + MouseDoubleClick = 10, + /// /// Input Received. /// InputReceived = 12, - + + /// + /// Input Navigation (LEFT, RIGHT, UP, DOWN, TAB_NEXT, TAB_PREV, TAB_BOTH_NEXT, TAB_BOTH_PREV, PAGEUP, PAGEDOWN). + /// + InputNavigation = 13, + + /// + /// InputBase Input Received (AtkComponentTextInput and AtkComponentNumericInput).
+ /// For example, this is fired for moving the text cursor, deletion of a character and inserting a new line. + ///
+ InputBaseInputReceived = 15, + /// /// Focus Start. /// FocusStart = 18, - + /// /// Focus Stop. /// FocusStop = 19, - + /// - /// Button Press, sent on MouseDown on Button. + /// Resize (ChatLogPanel). + /// + Resize = 19, + + /// + /// AtkComponentButton Press, sent on MouseDown on Button. /// ButtonPress = 23, - + /// - /// Button Release, sent on MouseUp and MouseOut. + /// AtkComponentButton Release, sent on MouseUp and MouseOut. /// ButtonRelease = 24, - + /// - /// Button Click, sent on MouseUp and MouseClick on button. + /// AtkComponentButton Click, sent on MouseUp and MouseClick on button. /// ButtonClick = 25, - + /// - /// List Item RollOver. + /// Value Update (NumericInput, ScrollBar, etc.) + /// + ValueUpdate = 27, + + /// + /// AtkComponentSlider Value Update. + /// + SliderValueUpdate = 29, + + /// + /// AtkComponentSlider Released. + /// + SliderReleased = 30, + + /// + /// AtkComponentList RollOver. /// ListItemRollOver = 33, - + /// - /// List Item Roll Out. + /// AtkComponentList Roll Out. /// ListItemRollOut = 34, - + /// - /// List Item Toggle. + /// AtkComponentList Click. /// - ListItemToggle = 35, - + ListItemClick = 35, + /// - /// Drag Drop Begin. + /// AtkComponentList Toggle. + /// + [Obsolete("Use ListItemClick")] + ListItemToggle = 35, + + /// + /// AtkComponentList Double Click. + /// + ListItemDoubleClick = 36, + + /// + /// AtkComponentList Select. + /// + ListItemSelect = 38, + + /// + /// AtkComponentDragDrop Begin. /// Sent on MouseDown over a draggable icon (will NOT send for a locked icon). /// - DragDropBegin = 47, - + DragDropBegin = 50, + /// - /// Drag Drop Insert. + /// AtkComponentDragDrop End. + /// + DragDropEnd = 51, + + /// + /// AtkComponentDragDrop Insert. /// Sent when dropping an icon into a hotbar/inventory slot or similar. /// - DragDropInsert = 50, - + DragDropInsert = 53, + /// - /// Drag Drop Roll Over. + /// AtkComponentDragDrop Roll Over. /// - DragDropRollOver = 52, - + DragDropRollOver = 55, + /// - /// Drag Drop Roll Out. + /// AtkComponentDragDrop Roll Out. /// - DragDropRollOut = 53, - + DragDropRollOut = 56, + /// - /// Drag Drop Discard. + /// AtkComponentDragDrop Discard. /// Sent when dropping an icon into empty screenspace, eg to remove an action from a hotBar. /// - DragDropDiscard = 54, - + DragDropDiscard = 57, + /// /// Drag Drop Unknown. /// - [Obsolete("Use DragDropDiscard")] + [Obsolete("Use DragDropDiscard", true)] DragDropUnk54 = 54, - + /// - /// Drag Drop Cancel. + /// AtkComponentDragDrop Cancel. /// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon. /// - DragDropCancel = 55, - + DragDropCancel = 58, + /// /// Drag Drop Unknown. /// - [Obsolete("Use DragDropCancel")] + [Obsolete("Use DragDropCancel", true)] DragDropUnk55 = 55, - + /// - /// Icon Text Roll Over. + /// AtkComponentIconText Roll Over. /// - IconTextRollOver = 56, - + IconTextRollOver = 59, + /// - /// Icon Text Roll Out. + /// AtkComponentIconText Roll Out. /// - IconTextRollOut = 57, - + IconTextRollOut = 60, + /// - /// Icon Text Click. + /// AtkComponentIconText Click. /// - IconTextClick = 58, - + IconTextClick = 61, + /// - /// Window Roll Over. + /// AtkDialogue Close. /// - WindowRollOver = 67, - + DialogueClose = 62, + /// - /// Window Roll Out. + /// AtkDialogue Submit. /// - WindowRollOut = 68, - + DialogueSubmit = 63, + /// - /// Window Change Scale. + /// AtkTimer Tick. /// - WindowChangeScale = 69, + TimerTick = 64, + + /// + /// AtkTimer End. + /// + TimerEnd = 65, + + /// + /// AtkSimpleTween Progress. + /// + TweenProgress = 67, + + /// + /// AtkSimpleTween Complete. + /// + TweenComplete = 68, + + /// + /// AtkAddonControl Child Addon Attached. + /// + ChildAddonAttached = 69, + + /// + /// AtkComponentWindow Roll Over. + /// + WindowRollOver = 70, + + /// + /// AtkComponentWindow Roll Out. + /// + WindowRollOut = 71, + + /// + /// AtkComponentWindow Change Scale. + /// + WindowChangeScale = 72, + + /// + /// AtkTextNode Link Mouse Click. + /// + LinkMouseClick = 75, + + /// + /// AtkTextNode Link Mouse Over. + /// + LinkMouseOver = 76, + + /// + /// AtkTextNode Link Mouse Out. + /// + LinkMouseOut = 77, } diff --git a/Dalamud/Game/Addon/Events/PluginEventController.cs b/Dalamud/Game/Addon/Events/PluginEventController.cs index 403a812db..0b1491e77 100644 --- a/Dalamud/Game/Addon/Events/PluginEventController.cs +++ b/Dalamud/Game/Addon/Events/PluginEventController.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Dalamud.Game.Gui; using Dalamud.Logging.Internal; -using Dalamud.Memory; using Dalamud.Plugin.Services; +using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -26,7 +26,7 @@ internal unsafe class PluginEventController : IDisposable } private AddonEventListener EventListener { get; init; } - + private List Events { get; } = new(); /// @@ -37,6 +37,7 @@ internal unsafe class PluginEventController : IDisposable /// The Event Type. /// The delegate to call when invoking this event. /// IAddonEventHandle used to remove the event. + [Obsolete("Use AddEvent that uses AddonEventDelegate instead of AddonEventHandler")] public IAddonEventHandle AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventHandler handler) { var node = (AtkResNode*)atkResNode; @@ -44,7 +45,7 @@ internal unsafe class PluginEventController : IDisposable var eventType = (AtkEventType)atkEventType; var eventId = this.GetNextParamKey(); var eventGuid = Guid.NewGuid(); - + var eventHandle = new AddonEventHandle { AddonName = addon->NameString, @@ -52,11 +53,54 @@ internal unsafe class PluginEventController : IDisposable EventType = atkEventType, EventGuid = eventGuid, }; - + var eventEntry = new AddonEventEntry { Addon = atkUnitBase, Handler = handler, + Delegate = null, + Node = atkResNode, + EventType = atkEventType, + ParamKey = eventId, + Handle = eventHandle, + }; + + Log.Verbose($"Adding Event. {eventEntry.LogString}"); + this.EventListener.RegisterEvent(addon, node, eventType, eventId); + this.Events.Add(eventEntry); + + return eventHandle; + } + + /// + /// Adds a tracked event. + /// + /// The Parent addon for the event. + /// The Node for the event. + /// The Event Type. + /// The delegate to call when invoking this event. + /// IAddonEventHandle used to remove the event. + public IAddonEventHandle AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventDelegate eventDelegate) + { + var node = (AtkResNode*)atkResNode; + var addon = (AtkUnitBase*)atkUnitBase; + var eventType = (AtkEventType)atkEventType; + var eventId = this.GetNextParamKey(); + var eventGuid = Guid.NewGuid(); + + var eventHandle = new AddonEventHandle + { + AddonName = addon->NameString, + ParamKey = eventId, + EventType = atkEventType, + EventGuid = eventGuid, + }; + + var eventEntry = new AddonEventEntry + { + Addon = atkUnitBase, + Delegate = eventDelegate, + Handler = null, Node = atkResNode, EventType = atkEventType, ParamKey = eventId, @@ -92,14 +136,14 @@ internal unsafe class PluginEventController : IDisposable if (this.Events.Where(entry => entry.AddonName == addonName).ToList() is { Count: not 0 } events) { Log.Verbose($"Addon: {addonName} is Finalizing, removing {events.Count} events."); - + foreach (var registeredEvent in events) { this.RemoveEvent(registeredEvent.Handle); } } } - + /// public void Dispose() { @@ -107,7 +151,7 @@ internal unsafe class PluginEventController : IDisposable { this.RemoveEvent(registeredEvent.Handle); } - + this.EventListener.Dispose(); } @@ -120,7 +164,7 @@ internal unsafe class PluginEventController : IDisposable throw new OverflowException($"uint.MaxValue number of ParamKeys used for this event controller."); } - + /// /// Attempts to remove a tracked event from native UI. /// This method performs several safety checks to only remove events from a still active addon. @@ -139,20 +183,22 @@ internal unsafe class PluginEventController : IDisposable // Is our stored addon pointer the same as the active addon pointer? if (currentAddonPointer != eventEntry.Addon) return; - // Does this addon contain the node this event is for? (by address) + // Make sure the addon is not unloaded var atkUnitBase = (AtkUnitBase*)currentAddonPointer; + if (atkUnitBase->UldManager.LoadedState == AtkLoadState.Unloaded) return; + + // Does this addon contain the node this event is for? (by address) var nodeFound = false; - foreach (var index in Enumerable.Range(0, atkUnitBase->UldManager.NodeListCount)) + foreach (var node in atkUnitBase->UldManager.Nodes) { - var node = atkUnitBase->UldManager.NodeList[index]; - // If this node matches our node, then we know our node is still valid. - if (node is not null && (nint)node == eventEntry.Node) + if ((nint)node.Value == eventEntry.Node) { nodeFound = true; + break; } } - + // If we didn't find the node, we can't remove the event. if (!nodeFound) return; @@ -166,33 +212,45 @@ internal unsafe class PluginEventController : IDisposable var paramKeyMatches = currentEvent->Param == eventEntry.ParamKey; var eventListenerAddressMatches = (nint)currentEvent->Listener == this.EventListener.Address; var eventTypeMatches = currentEvent->State.EventType == eventType; - + if (paramKeyMatches && eventListenerAddressMatches && eventTypeMatches) { eventFound = true; break; } - + // Move to the next event. currentEvent = currentEvent->NextEvent; } - + // If we didn't find the event, we can't remove the event. if (!eventFound) return; // We have a valid addon, valid node, valid event, and valid key. this.EventListener.UnregisterEvent(atkResNode, eventType, eventEntry.ParamKey); } - + + [Api13ToDo("Remove invoke from eventInfo.Handler, and remove nullability from eventInfo.Delegate?.Invoke")] private void PluginEventListHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventPtr, AtkEventData* eventDataPtr) { try { if (eventPtr is null) return; if (this.Events.FirstOrDefault(handler => handler.ParamKey == eventParam) is not { } eventInfo) return; - + // We stored the AtkUnitBase* in EventData->Node, and EventData->Target contains the node that triggered the event. - eventInfo.Handler.Invoke((AddonEventType)eventType, (nint)eventPtr->Node, (nint)eventPtr->Target); + eventInfo.Handler?.Invoke((AddonEventType)eventType, (nint)eventPtr->Node, (nint)eventPtr->Target); + + eventInfo.Delegate?.Invoke((AddonEventType)eventType, new AddonEventData + { + AddonPointer = (nint)eventPtr->Node, + NodeTargetPointer = (nint)eventPtr->Target, + AtkEventDataPointer = (nint)eventDataPtr, + AtkEventListener = (nint)self, + AtkEventType = (AddonEventType)eventType, + Param = eventParam, + AtkEventPointer = (nint)eventPtr, + }); } catch (Exception exception) { diff --git a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs b/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs index aa684a644..297323b8f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs @@ -11,7 +11,7 @@ namespace Dalamud.Game.Addon.Lifecycle; internal class AddonSetupHook : IDisposable where T : Delegate { private readonly Reloaded.Hooks.AsmHook asmHook; - + private T? detour; private bool activated; @@ -30,22 +30,22 @@ internal class AddonSetupHook : IDisposable where T : Delegate "use64", $"mov r9, 0x{detourPtr:X8}", }; - + var opt = new AsmHookOptions { PreferRelativeJump = true, Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal, MaxOpcodeSize = 5, }; - + this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt); } /// - /// Gets a value indicating whether or not the hook is enabled. + /// Gets a value indicating whether the hook is enabled. /// public bool IsEnabled => this.asmHook.IsEnabled; - + /// /// Starts intercepting a call to the function. /// @@ -57,7 +57,7 @@ internal class AddonSetupHook : IDisposable where T : Delegate this.asmHook.Activate(); return; } - + this.asmHook.Enable(); } diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index c40744ca4..61cd88a05 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -8,6 +8,8 @@ using Dalamud.Configuration.Internal; using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface; using Dalamud.Interface.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; @@ -41,7 +43,7 @@ internal partial class ChatHandlers : IServiceType public string? LastLink { get; private set; } /// - /// Gets a value indicating whether or not auto-updates have already completed this session. + /// Gets a value indicating whether auto-updates have already completed this session. /// public bool IsAutoUpdateComplete { get; private set; } @@ -100,8 +102,6 @@ internal partial class ChatHandlers : IServiceType if (chatGui == null || pluginManager == null || dalamudInterface == null) return; - var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString(); - if (this.configuration.PrintDalamudWelcomeMsg) { chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetScmVersion()) @@ -116,15 +116,30 @@ internal partial class ChatHandlers : IServiceType } } - if (string.IsNullOrEmpty(this.configuration.LastVersion) || !assemblyVersion.StartsWith(this.configuration.LastVersion)) + if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Util.AssemblyVersion.StartsWith(this.configuration.LastVersion)) { + var linkPayload = chatGui.AddChatLinkHandler( + "dalamud", + 8459324, + (_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs)); + + var updateMessage = new SeStringBuilder() + .AddText(Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully!")) + .AddUiForeground(500) + .AddText(" [") + .Add(linkPayload) + .AddText(Loc.Localize("DalamudClickToViewChangelogs", " Click here to view the changelog.")) + .Add(RawPayload.LinkTerminator) + .AddText("]") + .AddUiForegroundOff(); + chatGui.Print(new XivChatEntry { - Message = Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully! Please check the discord for a full changelog."), + Message = updateMessage.Build(), Type = XivChatType.Notice, }); - this.configuration.LastVersion = assemblyVersion; + this.configuration.LastVersion = Util.AssemblyVersion; this.configuration.QueueSave(); } diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index da9873d8d..5101657ba 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -158,7 +158,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState ConditionFlag.NormalConditions, ConditionFlag.Jumping, ConditionFlag.Mounted, - ConditionFlag.UsingParasol]); + ConditionFlag.UsingFashionAccessory]); blockingFlag = blockingConditions.FirstOrDefault(); return blockingFlag == 0; diff --git a/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs b/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs index 5b7bd2145..ef6649d7d 100644 --- a/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs +++ b/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs @@ -5,6 +5,8 @@ namespace Dalamud.Game.ClientState.Conditions; /// /// These come from LogMessage (somewhere) and directly map to each state field managed by the client. As of 5.25, it maps to /// LogMessage row 7700 and onwards, which can be checked by looking at the Condition sheet and looking at what column 2 maps to. +/// +/// The first 24 conditions are the local players CharacterModes. /// public enum ConditionFlag { @@ -176,11 +178,20 @@ public enum ConditionFlag /// /// Unable to execute command while occupied. /// + /// + /// Observed during Materialize (Desynthesis, Materia Extraction, Aetherial Reduction) and Repair. + /// Occupied39 = 39, /// /// Unable to execute command while crafting. /// + ExecutingCraftingAction = 40, + + /// + /// Unable to execute command while crafting. + /// + [Obsolete("Renamed to ExecutingCraftingAction.")] Crafting40 = 40, /// @@ -191,6 +202,13 @@ public enum ConditionFlag /// /// Unable to execute command while gathering. /// + /// Includes fishing. + ExecutingGatheringAction = 42, + + /// + /// Unable to execute command while gathering. + /// + [Obsolete("Renamed to ExecutingGatheringAction.")] Gathering42 = 42, /// @@ -220,8 +238,14 @@ public enum ConditionFlag /// /// Unable to execute command while auto-run is active. /// + [Obsolete("To avoid confusion, renamed to UsingChocoboTaxi.")] AutorunActive = 49, + /// + /// Unable to execute command while auto-run is active. + /// + UsingChocoboTaxi = 49, + /// /// Unable to execute command while occupied. /// @@ -261,8 +285,14 @@ public enum ConditionFlag /// /// Unable to execute command at this time. /// + [Obsolete("Renamed to MountOrOrnamentTransition.")] Unknown57 = 57, + /// + /// Unable to execute command at this time. + /// + MountOrOrnamentTransition = 57, + /// /// Unable to execute command while watching a cutscene. /// @@ -331,6 +361,9 @@ public enum ConditionFlag /// /// Unable to execute command while mounting. /// + /// + /// Observed in Cosmic Exploration while using the actions Astrodrill (only briefly) and Solar Flarethrower. + /// Mounting71 = 71, /// @@ -398,7 +431,10 @@ public enum ConditionFlag /// ParticipatingInCrossWorldPartyOrAlliance = 84, - // Unknown85 = 85, + /// + /// Observed in Cosmic Exploration while gathering during a stellar mission. + /// + Unknown85 = 85, /// /// Unable to execute command while playing duty record. @@ -430,7 +466,7 @@ public enum ConditionFlag /// [Obsolete("Use InDutyQueue")] BoundToDuty97 = 91, - + /// /// Unable to execute command while bound by duty. /// Specifically triggered when you are in a queue for a duty but not inside a duty. @@ -450,8 +486,14 @@ public enum ConditionFlag /// /// Unable to execute command while using a parasol. /// + [Obsolete("Renamed to UsingFashionAccessory.")] UsingParasol = 94, + /// + /// Unable to execute command while using a fashion accessory. + /// + UsingFashionAccessory = 94, + /// /// Unable to execute command while bound by duty. /// @@ -460,6 +502,9 @@ public enum ConditionFlag /// /// Cannot execute at this time. /// + /// + /// Observed in Cosmic Exploration while participating in MechaEvent. + /// Unknown96 = 96, /// @@ -481,4 +526,22 @@ public enum ConditionFlag /// Unable to execute command while editing a portrait. /// EditingPortrait = 100, + + /// + /// Cannot execute at this time. + /// + /// + /// Observed in Cosmic Exploration, in mech flying to FATE or during Cosmoliner use. Maybe ClientPath related. + /// + Unknown101 = 101, + + /// + /// Unable to execute command while undertaking a duty. + /// + /// + /// Used in Cosmic Exploration. + /// + PilotingMech = 102, + + // Unknown103 = 103, } diff --git a/Dalamud/Game/ClientState/Fates/Fate.cs b/Dalamud/Game/ClientState/Fates/Fate.cs index f48e66ad6..2da2dde9d 100644 --- a/Dalamud/Game/ClientState/Fates/Fate.cs +++ b/Dalamud/Game/ClientState/Fates/Fate.cs @@ -3,6 +3,7 @@ using System.Numerics; using Dalamud.Data; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Memory; +using Dalamud.Utility; using Lumina.Excel; @@ -69,13 +70,13 @@ public interface IFate : IEquatable byte Progress { get; } /// - /// Gets a value indicating whether or not this has a EXP bonus. + /// Gets a value indicating whether this has a EXP bonus. /// [Obsolete($"Use {nameof(HasBonus)} instead")] bool HasExpBonus { get; } /// - /// Gets a value indicating whether or not this has a bonus. + /// Gets a value indicating whether this has a bonus. /// bool HasBonus { get; } @@ -222,8 +223,8 @@ internal unsafe partial class Fate : IFate public byte Progress => this.Struct->Progress; /// - [Obsolete($"Use {nameof(HasBonus)} instead")] - public bool HasExpBonus => this.Struct->IsExpBonus; + [Api13ToDo("Remove")] + public bool HasExpBonus => this.HasBonus; /// public bool HasBonus => this.Struct->IsBonus; @@ -249,5 +250,5 @@ internal unsafe partial class Fate : IFate /// /// Gets the territory this is located in. /// - public RowRef TerritoryType => LuminaUtils.CreateRef(this.Struct->MapMarkers[0].TerritoryId); + public RowRef TerritoryType => LuminaUtils.CreateRef(this.Struct->MapMarkers[0].MapMarkerData.TerritoryTypeId); } diff --git a/Dalamud/Game/ClientState/JobGauge/Types/BLMGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/BLMGauge.cs index c4058132a..ad0cdd9e1 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/BLMGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/BLMGauge.cs @@ -45,19 +45,19 @@ public unsafe class BLMGauge : JobGaugeBase this.Struct->AstralSoulStacks; /// - /// Gets a value indicating whether or not the player is in Umbral Ice. + /// Gets a value indicating whether the player is in Umbral Ice. /// /// true or false. public bool InUmbralIce => this.Struct->ElementStance < 0; /// - /// Gets a value indicating whether or not the player is in Astral fire. + /// Gets a value indicating whether the player is in Astral fire. /// /// true or false. public bool InAstralFire => this.Struct->ElementStance > 0; /// - /// Gets a value indicating whether or not Enochian is active. + /// Gets a value indicating whether Enochian is active. /// /// true or false. public bool IsEnochianActive => this.Struct->EnochianActive; diff --git a/Dalamud/Game/ClientState/JobGauge/Types/PCTGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/PCTGauge.cs index db9d51dc4..d31a22702 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/PCTGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/PCTGauge.cs @@ -14,7 +14,7 @@ public unsafe class PCTGauge : JobGaugeBase /// Initializes a new instance of the class. /// /// Address of the job gauge. - internal PCTGauge(IntPtr address) + internal PCTGauge(IntPtr address) : base(address) { } @@ -28,29 +28,29 @@ public unsafe class PCTGauge : JobGaugeBase /// Gets the amount of paint the player has. /// public byte Paint => Struct->Paint; - + /// - /// Gets a value indicating whether or not a creature motif is drawn. + /// Gets a value indicating whether a creature motif is drawn. /// public bool CreatureMotifDrawn => Struct->CreatureMotifDrawn; /// - /// Gets a value indicating whether or not a weapon motif is drawn. + /// Gets a value indicating whether a weapon motif is drawn. /// public bool WeaponMotifDrawn => Struct->WeaponMotifDrawn; /// - /// Gets a value indicating whether or not a landscape motif is drawn. + /// Gets a value indicating whether a landscape motif is drawn. /// public bool LandscapeMotifDrawn => Struct->LandscapeMotifDrawn; /// - /// Gets a value indicating whether or not a moogle portrait is ready. + /// Gets a value indicating whether a moogle portrait is ready. /// public bool MooglePortraitReady => Struct->MooglePortraitReady; - + /// - /// Gets a value indicating whether or not a madeen portrait is ready. + /// Gets a value indicating whether a madeen portrait is ready. /// public bool MadeenPortraitReady => Struct->MadeenPortraitReady; diff --git a/Dalamud/Game/ClientState/Objects/Enums/CustomizeIndex.cs b/Dalamud/Game/ClientState/Objects/Enums/CustomizeIndex.cs index 299583fd3..534ee6347 100644 --- a/Dalamud/Game/ClientState/Objects/Enums/CustomizeIndex.cs +++ b/Dalamud/Game/ClientState/Objects/Enums/CustomizeIndex.cs @@ -42,7 +42,7 @@ public enum CustomizeIndex HairStyle = 0x06, /// - /// Whether or not the character has hair highlights. + /// Whether the character has hair highlights. /// HasHighlights = 0x07, // negative to enable, positive to disable diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index fdaa5833b..b72238abe 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -45,6 +45,16 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma this.console.Invoke += this.ConsoleOnInvoke; } + /// + /// Published whenever a command is registered + /// + public event EventHandler? CommandAdded; + + /// + /// Published whenever a command is unregistered + /// + public event EventHandler? CommandRemoved; + /// public ReadOnlyDictionary Commands => new(this.commandMap); @@ -122,6 +132,12 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma return false; } + this.CommandAdded?.Invoke(this, new CommandEventArgs + { + Command = command, + CommandInfo = info, + }); + if (!this.commandAssemblyNameMap.TryAdd((command, info), loaderAssemblyName)) { this.commandMap.Remove(command, out _); @@ -144,6 +160,12 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma return false; } + this.CommandAdded?.Invoke(this, new CommandEventArgs + { + Command = command, + CommandInfo = info, + }); + return true; } @@ -155,7 +177,17 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma this.commandAssemblyNameMap.TryRemove(assemblyKeyValuePair.Key, out _); } - return this.commandMap.Remove(command, out _); + var removed = this.commandMap.Remove(command, out var info); + if (removed) + { + this.CommandRemoved?.Invoke(this, new CommandEventArgs + { + Command = command, + CommandInfo = info, + }); + } + + return removed; } /// @@ -204,6 +236,20 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma return this.ProcessCommand(command->ToString()) ? 0 : result; } + + /// + public class CommandEventArgs : EventArgs + { + /// + /// Gets the command string. + /// + public string Command { get; init; } + + /// + /// Gets the command info. + /// + public IReadOnlyCommandInfo CommandInfo { get; init; } + } } /// @@ -268,7 +314,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand } else { - Log.Error($"Command {command} is already registered."); + Log.Error("Command {Command} is already registered.", command); } return false; @@ -287,7 +333,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand } else { - Log.Error($"Command {command} not found."); + Log.Error("Command {Command} not found.", command); } return false; diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 82c7f5f6c..88f9d0bb6 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -12,6 +12,7 @@ using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -47,7 +48,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework private readonly ConcurrentDictionary tickDelayedTaskCompletionSources = new(); - private ulong tickCounter; + private ulong tickCounter; [ServiceManager.ServiceConstructor] private unsafe Framework() @@ -504,14 +505,18 @@ internal sealed class Framework : IInternalDisposableService, IFramework #pragma warning restore SA1015 internal class FrameworkPluginScoped : IInternalDisposableService, IFramework { + private readonly PluginErrorHandler pluginErrorHandler; + [ServiceManager.ServiceDependency] private readonly Framework frameworkService = Service.Get(); /// /// Initializes a new instance of the class. /// - internal FrameworkPluginScoped() + /// Error handler instance. + internal FrameworkPluginScoped(PluginErrorHandler pluginErrorHandler) { + this.pluginErrorHandler = pluginErrorHandler; this.frameworkService.Update += this.OnUpdateForward; } @@ -604,7 +609,7 @@ internal class FrameworkPluginScoped : IInternalDisposableService, IFramework } else { - this.Update?.Invoke(framework); + this.pluginErrorHandler.InvokeAndCatch(this.Update, $"{nameof(IFramework)}::{nameof(IFramework.Update)}", framework); } } } diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 791cbb97a..721070d9b 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -220,6 +220,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui /// The ID of the command to run. /// The command action itself. /// A payload for handling. + [Api13ToDo("Plugins should not specify their own command IDs here. We should assign them ourselves.")] internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action commandAction) { var payload = new DalamudLinkPayload { Plugin = pluginName, CommandId = commandId }; diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index c6208fb2f..6f3f9a8dd 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -269,7 +269,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar /// Check whether an entry with the specified title exists. /// /// The title to check for. - /// Whether or not an entry with that title is registered. + /// Whether an entry with that title is registered. internal bool HasEntry(string title) { var found = false; diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index 49a2cbb73..26708eb4c 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -17,32 +17,32 @@ public interface IReadOnlyDtrBarEntry /// Gets the title of this entry. /// public string Title { get; } - + /// /// Gets a value indicating whether this entry has a click action. /// public bool HasClickAction { get; } - + /// /// Gets the text of this entry. /// public SeString? Text { get; } - + /// /// Gets a tooltip to be shown when the user mouses over the dtr entry. /// public SeString? Tooltip { get; } - + /// /// Gets a value indicating whether this entry should be shown. /// public bool Shown { get; } - + /// - /// Gets a value indicating whether or not the user has hidden this entry from view through the Dalamud settings. + /// Gets a value indicating whether the user has hidden this entry from view through the Dalamud settings. /// public bool UserHidden { get; } - + /// /// Triggers the click action of this entry. /// @@ -59,22 +59,22 @@ public interface IDtrBarEntry : IReadOnlyDtrBarEntry /// Gets or sets the text of this entry. /// public new SeString? Text { get; set; } - + /// /// Gets or sets a tooltip to be shown when the user mouses over the dtr entry. /// public new SeString? Tooltip { get; set; } - + /// /// Gets or sets a value indicating whether this entry is visible. /// public new bool Shown { get; set; } - + /// /// Gets or sets a action to be invoked when the user clicks on the dtr entry. /// public Action? OnClick { get; set; } - + /// /// Remove this entry from the bar. /// You will need to re-acquire it from DtrBar to reuse it. @@ -121,7 +121,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry /// public SeString? Tooltip { get; set; } - + /// /// Gets or sets a action to be invoked when the user clicks on the dtr entry. /// @@ -145,14 +145,14 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry } /// - [Api12ToDo("Maybe make this config scoped to internalname?")] + [Api13ToDo("Maybe make this config scoped to internalname?")] public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; /// /// Gets or sets the internal text node of this entry. /// internal AtkTextNode* TextNode { get; set; } - + /// /// Gets or sets the storage for the text of this entry. /// @@ -171,7 +171,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry /// /// Gets or sets a value indicating whether this entry has just been added. /// - internal bool Added { get; set; } + internal bool Added { get; set; } /// /// Gets or sets the plugin that owns this entry. @@ -183,7 +183,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry { if (this.OnClick == null) return false; - + this.OnClick.Invoke(); return true; } diff --git a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs index 0edbd09ee..2b8325927 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs @@ -94,22 +94,37 @@ public enum FlyTextKind : int /// /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle. - /// Added in 7.2, usage currently unknown. /// + [Obsolete("Use Dataset instead", true)] Unknown16 = 16, /// - /// Val1 in serif font, Text2 in sans-serif as subtitle. - /// Added in 7.2, usage currently unknown. + /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle. /// + Dataset = 16, + + /// + /// Val1 in serif font, Text2 in sans-serif as subtitle. + /// + [Obsolete("Use Knowledge instead", true)] Unknown17 = 17, /// /// Val1 in serif font, Text2 in sans-serif as subtitle. - /// Added in 7.2, usage currently unknown. /// + Knowledge = 17, + + /// + /// Val1 in serif font, Text2 in sans-serif as subtitle. + /// + [Obsolete("Use PhantomExp instead", true)] Unknown18 = 18, + /// + /// Val1 in serif font, Text2 in sans-serif as subtitle. + /// + PhantomExp = 18, + /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle. /// diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index a28221f86..a1e44918a 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -257,7 +257,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui /// /// Indicates if the game is in the lobby scene (title screen, chara select, chara make, aesthetician etc.). /// - /// A value indicating whether or not the game is in the lobby scene. + /// A value indicating whether the game is in the lobby scene. internal bool IsInLobby() => RaptureAtkModule.Instance()->CurrentUIScene.StartsWith("LobbyMain"u8); /// diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs index a398bdb82..0093a1b40 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs @@ -6,7 +6,7 @@ namespace Dalamud.Game.Gui.NamePlate; /// A part builder for constructing and setting quoted nameplate fields (i.e. free company tag and title). /// /// The field type which should be set. -/// Whether or not this is a Free Company part. +/// Whether this is a Free Company part. /// /// This class works as a lazy writer initialized with empty parts, where an empty part signifies no change should be /// performed. Only after all handler processing is complete does it write out any parts which were set to the @@ -53,7 +53,7 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany return; var sb = new SeStringBuilder(); - if (this.OuterWrap is { Item1: var outerLeft }) + if (this.OuterWrap is { Item1: { } outerLeft }) { sb.Append(outerLeft); } @@ -67,7 +67,7 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany sb.Append(isFreeCompany ? " «" : "《"); } - if (this.TextWrap is { Item1: var left, Item2: var right }) + if (this.TextWrap is { Item1: { } left, Item2: { } right }) { sb.Append(left); sb.Append(this.Text ?? this.GetStrippedField(handler)); @@ -87,7 +87,7 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany sb.Append(isFreeCompany ? "»" : "》"); } - if (this.OuterWrap is { Item2: var outerRight }) + if (this.OuterWrap is { Item2: { } outerRight }) { sb.Append(outerRight); } diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs index 2906005da..7ec178795 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs @@ -35,7 +35,7 @@ public class NamePlateSimpleParts(NamePlateStringField field) if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer) return; - if (this.TextWrap is { Item1: var left, Item2: var right }) + if (this.TextWrap is { Item1: { } left, Item2: { } right }) { var sb = new SeStringBuilder(); sb.Append(left); diff --git a/Dalamud/Game/Internal/AntiDebug.cs b/Dalamud/Game/Internal/AntiDebug.cs deleted file mode 100644 index 48b8688a1..000000000 --- a/Dalamud/Game/Internal/AntiDebug.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Collections.Generic; - -using Dalamud.Utility; - -#if !DEBUG -using Dalamud.Configuration.Internal; -#endif -using Serilog; - -namespace Dalamud.Game.Internal; - -/// -/// This class disables anti-debug functionality in the game client. -/// -[ServiceManager.EarlyLoadedService] -internal sealed class AntiDebug : IInternalDisposableService -{ - private readonly byte[] nop = [0x31, 0xC0, 0x90, 0x90, 0x90, 0x90]; - private byte[]? original; - private IntPtr debugCheckAddress; - - [ServiceManager.ServiceConstructor] - private AntiDebug(TargetSigScanner sigScanner) - { - try - { - // This sig has to be the call site in Framework_Tick - this.debugCheckAddress = sigScanner.ScanText("FF 15 ?? ?? ?? ?? 85 C0 74 13 41"); - } - catch (KeyNotFoundException) - { - this.debugCheckAddress = IntPtr.Zero; - } - - Log.Verbose($"Debug check address {Util.DescribeAddress(this.debugCheckAddress)}"); - - if (!this.IsEnabled) - { -#if DEBUG - this.Enable(); -#else - if (Service.Get().IsAntiAntiDebugEnabled) - this.Enable(); -#endif - } - } - - /// Finalizes an instance of the class. - ~AntiDebug() => ((IInternalDisposableService)this).DisposeService(); - - /// - /// Gets a value indicating whether the anti-debugging is enabled. - /// - public bool IsEnabled { get; private set; } = false; - - /// - void IInternalDisposableService.DisposeService() => this.Disable(); - - /// - /// Enables the anti-debugging by overwriting code in memory. - /// - public void Enable() - { - if (this.IsEnabled) - return; - - this.original = new byte[this.nop.Length]; - if (this.debugCheckAddress != IntPtr.Zero && !this.IsEnabled) - { - Log.Information($"Overwriting debug check at {Util.DescribeAddress(this.debugCheckAddress)}"); - SafeMemory.ReadBytes(this.debugCheckAddress, this.nop.Length, out this.original); - SafeMemory.WriteBytes(this.debugCheckAddress, this.nop); - } - else - { - Log.Information("Debug check already overwritten?"); - } - - this.IsEnabled = true; - } - - /// - /// Disable the anti-debugging by reverting the overwritten code in memory. - /// - public void Disable() - { - if (!this.IsEnabled) - return; - - if (this.debugCheckAddress != IntPtr.Zero && this.original != null) - { - Log.Information($"Reverting debug check at {Util.DescribeAddress(this.debugCheckAddress)}"); - SafeMemory.WriteBytes(this.debugCheckAddress, this.original); - } - else - { - Log.Information("Debug check was not overwritten?"); - } - - this.IsEnabled = false; - } -} diff --git a/Dalamud/Game/Internal/Completion.cs b/Dalamud/Game/Internal/Completion.cs new file mode 100644 index 000000000..01c9c99c5 --- /dev/null +++ b/Dalamud/Game/Internal/Completion.cs @@ -0,0 +1,322 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Game.Command; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using Dalamud.Utility; + +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.Completion; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Internal; + +/// +/// This class adds dalamud and plugin commands to the chat box's autocompletion. +/// +[ServiceManager.EarlyLoadedService] +internal sealed unsafe class Completion : IInternalDisposableService +{ + // 0xFF is a magic group number that causes CompletionModule's internals to treat entries + // as raw strings instead of as lookups into an EXD sheet + private const int GroupNumber = 0xFF; + + [ServiceManager.ServiceDependency] + private readonly CommandManager commandManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + + private readonly Dictionary cachedCommands = []; + private readonly ConcurrentQueue addedCommands = []; + + private EntryStrings? dalamudCategory; + + private Hook? getSelection; + + // This is marked volatile since we set and check it from different threads. Instead of using a synchronization + // primitive, a volatile is sufficient since the absolute worst case is that we delay one extra frame to reset + // the list, which is fine + private volatile bool needsClear; + private bool disposed; + private nint wantedVtblPtr; + + /// + /// Initializes a new instance of the class. + /// + [ServiceManager.ServiceConstructor] + internal Completion() + { + this.commandManager.CommandAdded += this.OnCommandAdded; + this.commandManager.CommandRemoved += this.OnCommandRemoved; + + this.framework.Update += this.OnUpdate; + } + + /// Finalizes an instance of the class. + ~Completion() => this.Dispose(false); + + /// + void IInternalDisposableService.DisposeService() => this.Dispose(true); + + private static AtkUnitBase* FindOwningAddon(AtkComponentTextInput* component) + { + if (component == null) return null; + + var node = (AtkResNode*)component->OwnerNode; + if (node == null) return null; + + while (node->ParentNode != null) + node = node->ParentNode; + + foreach (var addon in RaptureAtkUnitManager.Instance()->AllLoadedUnitsList.Entries) + { + if (addon.Value->RootNode == node) + return addon; + } + + return null; + } + + private AtkComponentTextInput* GetActiveTextInput() + { + var mod = RaptureAtkModule.Instance(); + if (mod == null) return null; + + var basePtr = mod->TextInput.TargetTextInputEventInterface; + if (basePtr == null) return null; + + // Once CS has an implementation for multiple inheritance, we can remove this sig from dalamud + // as well as the nasty pointer arithmetic below. But for now, we need to do this manually. + // The AtkTextInputEventInterface* is the secondary base class for AtkComponentTextInput* + // so the pointer is sizeof(AtkComponentInputBase) into the object. We verify that we're looking + // at the object we think we are by confirming the pointed-to vtbl matches the known secondary vtbl for + // AtkComponentTextInput, and if it does, we can shift the pointer back to get the start of our text input + if (this.wantedVtblPtr == 0) + { + this.wantedVtblPtr = + Service.Get().GetStaticAddressFromSig( + "48 89 01 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 8B 48 68", + 4); + } + + var vtblPtr = *(nint*)basePtr; + if (vtblPtr != this.wantedVtblPtr) return null; + + // This needs to be updated if the layout/base order of AtkComponentTextInput changes + return (AtkComponentTextInput*)((AtkComponentInputBase*)basePtr - 1); + } + + private bool AllowCompletion(string cmd) + { + // this is one of our commands, let's see if we should allow this to be completed + var component = this.GetActiveTextInput(); + + // ContainingAddon or ContainingAddon2 aren't always populated, but they + // seem to be in any case where this is actually a completable AtkComponentTextInput + // In the worst case, we can walk the AtkNode tree- but let's try the easy pointers first + var addon = component->ContainingAddon; + if (addon == null) addon = component->ContainingAddon2; + if (addon == null) addon = FindOwningAddon(component); + + if (addon == null || addon->NameString != "ChatLog") + { + // we don't know what addon is completing, or we know it isn't ChatLog + // either way, we should just reject this completion + return false; + } + + // We're in ChatLog, so check if this is the start of the text input + // AtkComponentTextInput->UnkText1 is the evaluated version of the current text + // so if the command starts with that, then either it's empty or a prefix completion. + // In either case, we're happy to allow completion. + return cmd.StartsWith(component->UnkText1.StringPtr.ExtractText()); + } + + private void Dispose(bool disposing) + { + if (this.disposed) + return; + + if (disposing) + { + this.getSelection?.Disable(); + this.getSelection?.Dispose(); + this.framework.Update -= this.OnUpdate; + this.commandManager.CommandAdded -= this.OnCommandAdded; + this.commandManager.CommandRemoved -= this.OnCommandRemoved; + + this.dalamudCategory?.Dispose(); + this.ClearCachedCommands(); + } + + this.disposed = true; + } + + private void OnCommandAdded(object? sender, CommandManager.CommandEventArgs e) + { + if (e.CommandInfo.ShowInHelp) + this.addedCommands.Enqueue(e.Command); + } + + private void OnCommandRemoved(object? sender, CommandManager.CommandEventArgs e) => this.needsClear = true; + + private void OnUpdate(IFramework fw) + { + var atkModule = RaptureAtkModule.Instance(); + if (atkModule == null) return; + + var textInput = &atkModule->TextInput; + + if (textInput->CompletionModule == null) return; + + // Before we change _anything_ we need to check the state of the UI- if the completion list is open + // changes to the underlying data are extremely unsafe, so we'll just wait until the next frame + // worst case, someone tries to complete a command that _just_ got unloaded so it won't do anything + // but that's the same as making a typo, really + if (textInput->CompletionDepth > 0) return; + + // Create the category for Dalamud commands. + // This needs to be done here, since we cannot create Utf8Strings before the game + // has initialized (no allocator set up yet). + this.dalamudCategory ??= new EntryStrings("【Dalamud】"); + + this.LoadCommands(textInput->CompletionModule); + } + + private CategoryData* EnsureCategoryData(CompletionModule* module) + { + if (module == null) return null; + + if (this.getSelection == null) + { + this.getSelection = Hook.FromAddress( + (IntPtr)module->VirtualTable->GetSelection, + this.GetSelectionDetour); + this.getSelection.Enable(); + } + + for (var i = 0; i < module->CategoryNames.Count; i++) + { + if (module->CategoryNames[i].AsReadOnlySeStringSpan().ContainsText("【Dalamud】"u8)) + { + return module->CategoryData[i]; + } + } + + // Create the category since we don't have one + var categoryData = (CategoryData*)Memory.MemoryHelper.GameAllocateDefault((ulong)sizeof(CategoryData)); + categoryData->Ctor(GroupNumber, 0xFF); + module->AddCategoryData(GroupNumber, this.dalamudCategory!.Display->StringPtr, + this.dalamudCategory.Match->StringPtr, categoryData); + + return categoryData; + } + + private void ClearCachedCommands() + { + if (this.cachedCommands.Count == 0) + return; + + foreach (var entry in this.cachedCommands.Values) + { + entry.Dispose(); + } + + this.cachedCommands.Clear(); + } + + private void LoadCommands(CompletionModule* completionModule) + { + if (completionModule == null) return; + if (completionModule->CategoryNames.Count == 0) return; // We want this data populated first + + if (this.needsClear && this.cachedCommands.Count > 0) + { + this.needsClear = false; + completionModule->ClearCompletionData(); + this.ClearCachedCommands(); + return; + } + + var catData = this.EnsureCategoryData(completionModule); + if (catData == null) return; + + if (catData->CompletionData.Count == 0) + { + var inputCommands = this.commandManager.Commands.Where(pair => pair.Value.ShowInHelp); + foreach (var (cmd, _) in inputCommands) + AddEntry(cmd); + catData->SortEntries(); + + return; + } + + var needsSort = false; + while (this.addedCommands.TryDequeue(out var cmd)) + { + needsSort = true; + AddEntry(cmd); + } + + if (needsSort) + catData->SortEntries(); + + return; + + void AddEntry(string cmd) + { + if (this.cachedCommands.ContainsKey(cmd)) return; + + var cmdStr = new EntryStrings(cmd); + this.cachedCommands.Add(cmd, cmdStr); + completionModule->AddCompletionEntry( + GroupNumber, + 0xFF, + cmdStr.Display->StringPtr, + cmdStr.Match->StringPtr, + 0xFF); + } + } + + private int GetSelectionDetour(CompletionModule* thisPtr, CategoryData.CompletionDataStruct* dataStructs, int index, Utf8String* outputString, Utf8String* outputDisplayString) + { + var ret = this.getSelection!.Original.Invoke(thisPtr, dataStructs, index, outputString, outputDisplayString); + if (ret != -2 || outputString == null) return ret; + + // -2 means it was a plain text final selection, so it might be ours + // Unfortunately, the code that uses this string mangles the color macro for some reason... + // We'll just strip those out since we don't need the color in the chatbox + var txt = outputString->StringPtr.ExtractText(); + if (!this.cachedCommands.ContainsKey(txt)) + return ret; + + if (!this.AllowCompletion(txt)) + { + outputString->Clear(); + if (outputDisplayString != null) outputDisplayString->Clear(); + return ret; + } + + outputString->SetString(txt + " "); + return ret; + } + + private class EntryStrings(string command) : IDisposable + { + public Utf8String* Display { get; } = + Utf8String.FromSequence(new SeStringBuilder().AddUiForeground(command, 539).BuiltString.EncodeWithNullTerminator()); + + public Utf8String* Match { get; } = Utf8String.FromString(command); + + public void Dispose() + { + this.Display->Dtor(true); + this.Match->Dtor(true); + } + } +} diff --git a/Dalamud/Game/Network/Internal/MarketBoardUploaders/MarketBoardItemRequest.cs b/Dalamud/Game/Network/Internal/MarketBoardUploaders/MarketBoardItemRequest.cs index a32a92b13..9c566687a 100644 --- a/Dalamud/Game/Network/Internal/MarketBoardUploaders/MarketBoardItemRequest.cs +++ b/Dalamud/Game/Network/Internal/MarketBoardUploaders/MarketBoardItemRequest.cs @@ -21,7 +21,7 @@ internal class MarketBoardItemRequest public uint Status { get; private set; } /// - /// Gets a value indicating whether or not this request was successful. + /// Gets a value indicating whether this request was successful. /// public bool Ok => this.Status == 0; diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index c0929fa84..7d6304655 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -248,7 +248,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService /// /// Disposes of managed and unmanaged resources. /// - /// Whether or not to execute the disposal. + /// Whether to execute the disposal. protected void Dispose(bool shouldDispose) { if (!shouldDispose) diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index 3422848f3..5aaf17f12 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -31,7 +31,7 @@ public class SigScanner : IDisposable, ISigScanner /// /// Initializes a new instance of the class using the main module of the current process. /// - /// Whether or not to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks. + /// Whether to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks. /// File used to cached signatures. public SigScanner(bool doCopy = false, FileInfo? cacheFile = null) : this(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile) @@ -42,7 +42,7 @@ public class SigScanner : IDisposable, ISigScanner /// Initializes a new instance of the class. /// /// The ProcessModule to be used for scanning. - /// Whether or not to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks. + /// Whether to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks. /// File used to cached signatures. public SigScanner(ProcessModule module, bool doCopy = false, FileInfo? cacheFile = null) { diff --git a/Dalamud/Game/TargetSigScanner.cs b/Dalamud/Game/TargetSigScanner.cs index 5c93fb4d4..f60c32d9a 100644 --- a/Dalamud/Game/TargetSigScanner.cs +++ b/Dalamud/Game/TargetSigScanner.cs @@ -19,7 +19,7 @@ internal class TargetSigScanner : SigScanner, IPublicDisposableService /// /// Initializes a new instance of the class. /// - /// Whether or not to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks. + /// Whether to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks. /// File used to cached signatures. public TargetSigScanner(bool doCopy = false, FileInfo? cacheFile = null) : base(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 83f8e241a..57040701c 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -18,12 +18,12 @@ using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Info; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Component.Text; +using FFXIVClientStructs.Interop; using Lumina.Data.Structs.Excel; using Lumina.Excel; @@ -35,6 +35,8 @@ using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; using AddonSheet = Lumina.Excel.Sheets.Addon; +using PlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState; +using StatusSheet = Lumina.Excel.Sheets.Status; namespace Dalamud.Game.Text.Evaluator; @@ -50,6 +52,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator { private static readonly ModuleLog Log = new("SeStringEvaluator"); + [ServiceManager.ServiceDependency] + private readonly ClientState.ClientState clientState = Service.Get(); + [ServiceManager.ServiceDependency] private readonly DataManager dataManager = Service.Get(); @@ -91,7 +96,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (str.IsTextOnly()) return new(str); - var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); + var lang = language ?? this.GetEffectiveClientLanguage(); // TODO: remove culture info toggling after supporting CultureInfo for SeStringBuilder.Append, // and then remove try...finally block (discard builder from the pool on exception) @@ -109,13 +114,22 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator } } + /// + public ReadOnlySeString EvaluateMacroString( + string macroString, + Span localParameters = default, + ClientLanguage? language = null) + { + return this.Evaluate(ReadOnlySeString.FromMacroString(macroString).AsSpan(), localParameters, language); + } + /// public ReadOnlySeString EvaluateFromAddon( uint addonId, Span localParameters = default, ClientLanguage? language = null) { - var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); + var lang = language ?? this.GetEffectiveClientLanguage(); if (!this.dataManager.GetExcelSheet(lang).TryGetRow(addonId, out var addonRow)) return default; @@ -129,7 +143,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator Span localParameters = default, ClientLanguage? language = null) { - var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); + var lang = language ?? this.GetEffectiveClientLanguage(); if (!this.dataManager.GetExcelSheet(lang).TryGetRow(lobbyId, out var lobbyRow)) return default; @@ -143,7 +157,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator Span localParameters = default, ClientLanguage? language = null) { - var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); + var lang = language ?? this.GetEffectiveClientLanguage(); if (!this.dataManager.GetExcelSheet(lang).TryGetRow(logMessageId, out var logMessageRow)) return default; @@ -154,7 +168,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator /// public string EvaluateActStr(ActionKind actionKind, uint id, ClientLanguage? language = null) => this.actStrCache.GetOrAdd( - new(actionKind, id, language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage()), + new(actionKind, id, language ?? this.GetEffectiveClientLanguage()), static (key, t) => t.EvaluateFromAddon(2026, [key.Kind.GetActStrId(key.Id)], key.Language) .ExtractText() .StripSoftHyphen(), @@ -163,7 +177,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator /// public string EvaluateObjStr(ObjectKind objectKind, uint id, ClientLanguage? language = null) => this.objStrCache.GetOrAdd( - new(objectKind, id, language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage()), + new(objectKind, id, language ?? this.GetEffectiveClientLanguage()), static (key, t) => t.EvaluateFromAddon(2025, [key.Kind.GetObjStrId(key.Id)], key.Language) .ExtractText() .StripSoftHyphen(), @@ -182,6 +196,18 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator private static uint ConvertRawToMapPosY(Lumina.Excel.Sheets.Map map, float y) => ConvertRawToMapPos(map, map.OffsetY, y); + private ClientLanguage GetEffectiveClientLanguage() + { + return this.dalamudConfiguration.EffectiveLanguage switch + { + "ja" => ClientLanguage.Japanese, + "en" => ClientLanguage.English, + "de" => ClientLanguage.German, + "fr" => ClientLanguage.French, + _ => this.clientState.ClientLanguage, + }; + } + private SeStringBuilder EvaluateAndAppendTo( SeStringBuilder builder, ReadOnlySeStringSpan str, @@ -445,7 +471,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (this.gameConfig.UiConfig.TryGetUInt("LogCrossWorldName", out var logCrossWorldName) && logCrossWorldName == 1) - context.Builder.Append((ReadOnlySeStringSpan)world.Name); + context.Builder.Append(new ReadOnlySeStringSpan(world.Name.GetPointer(0))); } return true; @@ -635,7 +661,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator { case false when digit == 0: continue; - case true when i % 3 == 0: + case true when MathF.Log10(i) % 3 == 2: this.ResolveStringExpression(in context, eSep); break; } @@ -711,84 +737,186 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator this.TryResolveUInt(in context, enu.Current, out eColParamValue); var resolvedSheetName = this.Evaluate(eSheetNameStr, context.LocalParameters, context.Language).ExtractText(); - - this.sheetRedirectResolver.Resolve(ref resolvedSheetName, ref eRowIdValue, ref eColIndexValue); + var originalRowIdValue = eRowIdValue; + var flags = this.sheetRedirectResolver.Resolve(ref resolvedSheetName, ref eRowIdValue, ref eColIndexValue); if (string.IsNullOrEmpty(resolvedSheetName)) return false; - if (!this.dataManager.Excel.SheetNames.Contains(resolvedSheetName)) + var text = this.FormatSheetValue(context.Language, resolvedSheetName, eRowIdValue, eColIndexValue, eColParamValue); + if (text.IsEmpty) return false; - if (!this.dataManager.GetExcelSheet(context.Language, resolvedSheetName) - .TryGetRow(eRowIdValue, out var row)) - return false; + this.AddSheetRedirectItemDecoration(context, ref text, flags, eRowIdValue); - if (eColIndexValue >= row.Columns.Count) - return false; + if (resolvedSheetName != "DescriptionString") + eColParamValue = originalRowIdValue; - var column = row.Columns[(int)eColIndexValue]; - switch (column.Type) + // Note: The link marker symbol is added by RaptureLogMessage, probably somewhere in it's Update function. + // It is not part of this generated link. + this.CreateSheetLink(context, resolvedSheetName, text, eRowIdValue, eColParamValue); + + return true; + } + + private ReadOnlySeString FormatSheetValue(ClientLanguage language, string sheetName, uint rowId, uint colIndex, uint colParam) + { + if (!this.dataManager.Excel.SheetNames.Contains(sheetName)) + return default; + + if (!this.dataManager.GetExcelSheet(language, sheetName) + .TryGetRow(rowId, out var row)) + return default; + + if (colIndex >= row.Columns.Count) + return default; + + var column = row.Columns[(int)colIndex]; + return column.Type switch { - case ExcelColumnDataType.String: - context.Builder.Append(this.Evaluate(row.ReadString(column.Offset), [eColParamValue], context.Language)); - return true; - case ExcelColumnDataType.Bool: - context.Builder.Append((row.ReadBool(column.Offset) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.Int8: - context.Builder.Append(row.ReadInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.UInt8: - context.Builder.Append(row.ReadUInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.Int16: - context.Builder.Append(row.ReadInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.UInt16: - context.Builder.Append(row.ReadUInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.Int32: - context.Builder.Append(row.ReadInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.UInt32: - context.Builder.Append(row.ReadUInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.Float32: - context.Builder.Append(row.ReadFloat32(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.Int64: - context.Builder.Append(row.ReadInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.UInt64: - context.Builder.Append(row.ReadUInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool0: - context.Builder.Append((row.ReadPackedBool(column.Offset, 0) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool1: - context.Builder.Append((row.ReadPackedBool(column.Offset, 1) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool2: - context.Builder.Append((row.ReadPackedBool(column.Offset, 2) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool3: - context.Builder.Append((row.ReadPackedBool(column.Offset, 3) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool4: - context.Builder.Append((row.ReadPackedBool(column.Offset, 4) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool5: - context.Builder.Append((row.ReadPackedBool(column.Offset, 5) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool6: - context.Builder.Append((row.ReadPackedBool(column.Offset, 6) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool7: - context.Builder.Append((row.ReadPackedBool(column.Offset, 7) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; + ExcelColumnDataType.String => this.Evaluate(row.ReadString(column.Offset), [colParam], language), + ExcelColumnDataType.Bool => (row.ReadBool(column.Offset) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.Int8 => row.ReadInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.UInt8 => row.ReadUInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.Int16 => row.ReadInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.UInt16 => row.ReadUInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.Int32 => row.ReadInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.UInt32 => row.ReadUInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.Float32 => row.ReadFloat32(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.Int64 => row.ReadInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.UInt64 => row.ReadUInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool0 => (row.ReadPackedBool(column.Offset, 0) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool1 => (row.ReadPackedBool(column.Offset, 1) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool2 => (row.ReadPackedBool(column.Offset, 2) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool3 => (row.ReadPackedBool(column.Offset, 3) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool4 => (row.ReadPackedBool(column.Offset, 4) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool5 => (row.ReadPackedBool(column.Offset, 5) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool6 => (row.ReadPackedBool(column.Offset, 6) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool7 => (row.ReadPackedBool(column.Offset, 7) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + _ => default, + }; + } + + private void AddSheetRedirectItemDecoration(in SeStringContext context, ref ReadOnlySeString text, SheetRedirectFlags flags, uint eRowIdValue) + { + if (!flags.HasFlag(SheetRedirectFlags.Item)) + return; + + var rarity = 1u; + var skipLink = false; + + if (flags.HasFlag(SheetRedirectFlags.EventItem)) + { + rarity = 8; + skipLink = true; + } + + var itemId = eRowIdValue; + + if (this.dataManager.GetExcelSheet(context.Language).TryGetRow(itemId, out var itemRow)) + { + rarity = itemRow.Rarity; + if (rarity == 0) + rarity = 1; + + if (itemRow.FilterGroup is 38 or 50) + skipLink = true; + } + + if (flags.HasFlag(SheetRedirectFlags.Collectible)) + { + itemId += 500000; + } + else if (flags.HasFlag(SheetRedirectFlags.HighQuality)) + { + itemId += 1000000; + } + + var sb = SeStringBuilder.SharedPool.Get(); + + sb.Append(this.EvaluateFromAddon(6, [rarity], context.Language)); + + if (!skipLink) + sb.PushLink(LinkMacroPayloadType.Item, itemId, rarity, 0u); // arg3 = some LogMessage flag based on LogKind RowId? => "89 5C 24 20 E8 ?? ?? ?? ?? 48 8B 1F" + + // there is code here for handling noun link markers (//), but i don't know why + + sb.Append(text); + + if (flags.HasFlag(SheetRedirectFlags.HighQuality) + && this.dataManager.GetExcelSheet(context.Language).TryGetRow(9, out var hqSymbol)) + { + sb.Append(hqSymbol.Text); + } + else if (flags.HasFlag(SheetRedirectFlags.Collectible) + && this.dataManager.GetExcelSheet(context.Language).TryGetRow(150, out var collectibleSymbol)) + { + sb.Append(collectibleSymbol.Text); + } + + if (!skipLink) + sb.PopLink(); + + text = sb.ToReadOnlySeString(); + SeStringBuilder.SharedPool.Return(sb); + } + + private void CreateSheetLink(in SeStringContext context, string resolvedSheetName, ReadOnlySeString text, uint eRowIdValue, uint eColParamValue) + { + switch (resolvedSheetName) + { + case "Achievement": + context.Builder.PushLink(LinkMacroPayloadType.Achievement, eRowIdValue, 0u, 0u, text.AsSpan()); + context.Builder.Append(text); + context.Builder.PopLink(); + return; + + case "HowTo": + context.Builder.PushLink(LinkMacroPayloadType.HowTo, eRowIdValue, 0u, 0u, text.AsSpan()); + context.Builder.Append(text); + context.Builder.PopLink(); + return; + + case "Status" when this.dataManager.GetExcelSheet(context.Language).TryGetRow(eRowIdValue, out var statusRow): + context.Builder.PushLink(LinkMacroPayloadType.Status, eRowIdValue, 0u, 0u, []); + + switch (statusRow.StatusCategory) + { + case 1: context.Builder.Append(this.EvaluateFromAddon(376)); break; // buff symbol + case 2: context.Builder.Append(this.EvaluateFromAddon(377)); break; // debuff symbol + } + + context.Builder.Append(text); + context.Builder.PopLink(); + return; + + case "AkatsukiNoteString": + context.Builder.PushLink(LinkMacroPayloadType.AkatsukiNote, eColParamValue, 0u, 0u, text.AsSpan()); + context.Builder.Append(text); + context.Builder.PopLink(); + return; + + case "DescriptionString" when eColParamValue > 0: + context.Builder.PushLink((LinkMacroPayloadType)11, eRowIdValue, eColParamValue, 0u, text.AsSpan()); + context.Builder.Append(text); + context.Builder.PopLink(); + return; + + case "WKSPioneeringTrailString": + context.Builder.PushLink((LinkMacroPayloadType)12, eRowIdValue, eColParamValue, 0u, text.AsSpan()); + context.Builder.Append(text); + context.Builder.PopLink(); + return; + + case "MKDLore": + context.Builder.PushLink((LinkMacroPayloadType)13, eRowIdValue, 0u, 0u, text.AsSpan()); + context.Builder.Append(text); + context.Builder.PopLink(); + return; + default: - return false; + context.Builder.Append(text); + return; } } @@ -938,9 +1066,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (p.Type == ReadOnlySePayloadType.Text) { - context.Builder.Append( - context.CultureInfo.TextInfo.ToTitleCase(Encoding.UTF8.GetString(p.Body.Span))); - + context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).ToUpper(true, true, false, context.Language)); continue; } @@ -1067,8 +1193,8 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var placeNameIdInt)) return false; - var instance = packedIds >> 0x10; - var mapId = packedIds & 0xFF; + var instance = packedIds >> 16; + var mapId = packedIds & 0xFFFF; if (this.dataManager.GetExcelSheet(context.Language) .TryGetRow(territoryTypeId, out var territoryTypeRow)) @@ -1356,8 +1482,6 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator var group = (uint)(e0Val + 1); var rowId = (uint)e1Val; - using var icons = new SeStringBuilderIconWrap(context.Builder, 54, 55); - if (!this.dataManager.GetExcelSheet(context.Language).TryGetFirst( row => row.Group == group && !row.LookupTable.IsEmpty, out var groupRow)) @@ -1382,6 +1506,8 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator return true; } + using var icons = new SeStringBuilderIconWrap(context.Builder, 54, 55); + // CategoryDataCache if (lookupTable.Equals("#")) { diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs index d6fd897b8..25cdf7f9f 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs @@ -31,7 +31,7 @@ public class ItemPayload : Payload /// Creates a payload representing an interactable item link for the specified item. /// /// The id of the item. - /// Whether or not the link should be for the high-quality variant of the item. + /// Whether the link should be for the high-quality variant of the item. /// An optional name to include in the item link. Typically this should /// be left as null, or set to the normal item name. Actual overrides are better done with the subsequent /// TextPayload that is a part of a full item link in chat. @@ -75,7 +75,7 @@ public class ItemPayload : Payload /// /// Kinds of items that can be fetched from this payload. /// - [Api12ToDo("Move this out of ItemPayload. It's used in other classes too.")] + [Api13ToDo("Move this out of ItemPayload. It's used in other classes too.")] public enum ItemKind : uint { /// @@ -142,7 +142,7 @@ public class ItemPayload : Payload : (RowRef)LuminaUtils.CreateRef(this.ItemId); /// - /// Gets a value indicating whether or not this item link is for a high-quality version of the item. + /// Gets a value indicating whether this item link is for a high-quality version of the item. /// [JsonProperty] public bool IsHQ => this.Kind == ItemKind.Hq; diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs index 8443e06ce..bf360ce34 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs @@ -46,7 +46,7 @@ public class UIForegroundPayload : Payload public override PayloadType Type => PayloadType.UIForeground; /// - /// Gets a value indicating whether or not this payload represents applying a foreground color, or disabling one. + /// Gets a value indicating whether this payload represents applying a foreground color, or disabling one. /// public bool IsEnabled => this.ColorKey != 0; diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs index d22318378..e54427073 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs @@ -64,7 +64,7 @@ public class UIGlowPayload : Payload } /// - /// Gets a value indicating whether or not this payload represents applying a glow color, or disabling one. + /// Gets a value indicating whether this payload represents applying a glow color, or disabling one. /// public bool IsEnabled => this.ColorKey != 0; diff --git a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs index e78ac2de8..d5080e6e8 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs @@ -113,7 +113,7 @@ public class SeStringBuilder /// Add an item link to the builder. /// /// The item ID. - /// Whether or not the item is high quality. + /// Whether the item is high quality. /// Override for the item's name. /// The current builder. public SeStringBuilder AddItemLink(uint itemId, bool isHq, string? itemNameOverride = null) => diff --git a/Dalamud/Hooking/AsmHook.cs b/Dalamud/Hooking/AsmHook.cs index f1ed7fd11..09ae336dc 100644 --- a/Dalamud/Hooking/AsmHook.cs +++ b/Dalamud/Hooking/AsmHook.cs @@ -20,7 +20,7 @@ public sealed class AsmHook : IDisposable, IDalamudHook private bool isEnabled = false; private DynamicMethod statsMethod; - + private Guid hookId = Guid.NewGuid(); /// @@ -89,7 +89,7 @@ public sealed class AsmHook : IDisposable, IDalamudHook } /// - /// Gets a value indicating whether or not the hook is enabled. + /// Gets a value indicating whether the hook is enabled. /// public bool IsEnabled { @@ -101,7 +101,7 @@ public sealed class AsmHook : IDisposable, IDalamudHook } /// - /// Gets a value indicating whether or not the hook has been disposed. + /// Gets a value indicating whether the hook has been disposed. /// public bool IsDisposed { get; private set; } diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index 51e62e918..faf4658a5 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -59,12 +59,12 @@ public abstract class Hook : IDalamudHook where T : Delegate => this.IsDisposed ? Marshal.GetDelegateForFunctionPointer(this.address) : this.Original; /// - /// Gets a value indicating whether or not the hook is enabled. + /// Gets a value indicating whether the hook is enabled. /// public virtual bool IsEnabled => throw new NotImplementedException(); /// - /// Gets a value indicating whether or not the hook has been disposed. + /// Gets a value indicating whether the hook has been disposed. /// public bool IsDisposed { get; private set; } @@ -90,6 +90,7 @@ public abstract class Hook : IDalamudHook where T : Delegate /// /// Starts intercepting a call to the function. /// + /// Hook is already disposed. public virtual void Enable() => throw new NotImplementedException(); /// diff --git a/Dalamud/Hooking/IDalamudHook.cs b/Dalamud/Hooking/IDalamudHook.cs index bffca242e..5ced572c0 100644 --- a/Dalamud/Hooking/IDalamudHook.cs +++ b/Dalamud/Hooking/IDalamudHook.cs @@ -11,12 +11,12 @@ public interface IDalamudHook : IDisposable public IntPtr Address { get; } /// - /// Gets a value indicating whether or not the hook is enabled. + /// Gets a value indicating whether the hook is enabled. /// public bool IsEnabled { get; } /// - /// Gets a value indicating whether or not the hook is disposed. + /// Gets a value indicating whether the hook is disposed. /// public bool IsDisposed { get; } diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs index c9b5562ba..92bc6e31a 100644 --- a/Dalamud/Hooking/Internal/CallHook.cs +++ b/Dalamud/Hooking/Internal/CallHook.cs @@ -15,10 +15,10 @@ namespace Dalamud.Hooking.Internal; /// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered. /// /// Delegate signature for this hook. -internal class CallHook : IDisposable where T : Delegate +internal class CallHook : IDalamudHook where T : Delegate { private readonly Reloaded.Hooks.AsmHook asmHook; - + private T? detour; private bool activated; @@ -29,7 +29,10 @@ internal class CallHook : IDisposable where T : Delegate /// Delegate to invoke. internal CallHook(nint address, T detour) { + ArgumentNullException.ThrowIfNull(detour); + this.detour = detour; + this.Address = address; var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour); var code = new[] @@ -38,22 +41,31 @@ internal class CallHook : IDisposable where T : Delegate $"mov rax, 0x{detourPtr:X8}", "call rax", }; - + var opt = new AsmHookOptions { PreferRelativeJump = true, Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal, MaxOpcodeSize = 5, }; - + this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt); } /// - /// Gets a value indicating whether or not the hook is enabled. + /// Gets a value indicating whether the hook is enabled. /// public bool IsEnabled => this.asmHook.IsEnabled; - + + /// + public IntPtr Address { get; } + + /// + public string BackendName => "Reloaded AsmHook"; + + /// + public bool IsDisposed => this.detour == null; + /// /// Starts intercepting a call to the function. /// @@ -65,7 +77,7 @@ internal class CallHook : IDisposable where T : Delegate this.asmHook.Activate(); return; } - + this.asmHook.Enable(); } diff --git a/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs b/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs index 6bd8efb65..8a53e664a 100644 --- a/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs +++ b/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs @@ -116,14 +116,7 @@ internal unsafe class FunctionPointerVariableHook : Hook } /// - public override bool IsEnabled - { - get - { - this.CheckDisposed(); - return this.enabled; - } - } + public override bool IsEnabled => !this.IsDisposed && this.enabled; /// public override string BackendName => "MinHook"; @@ -132,9 +125,7 @@ internal unsafe class FunctionPointerVariableHook : Hook public override void Dispose() { if (this.IsDisposed) - { return; - } this.Disable(); @@ -149,15 +140,13 @@ internal unsafe class FunctionPointerVariableHook : Hook /// public override void Enable() { - this.CheckDisposed(); - - if (this.enabled) - { - return; - } - lock (HookManager.HookEnableSyncRoot) { + this.CheckDisposed(); + + if (this.enabled) + return; + Marshal.WriteIntPtr(this.ppfnThunkJumpTarget, this.pfnDetour); this.enabled = true; } @@ -166,15 +155,14 @@ internal unsafe class FunctionPointerVariableHook : Hook /// public override void Disable() { - this.CheckDisposed(); - - if (!this.enabled) - { - return; - } - lock (HookManager.HookEnableSyncRoot) { + if (this.IsDisposed) + return; + + if (!this.enabled) + return; + Marshal.WriteIntPtr(this.ppfnThunkJumpTarget, this.pfnOriginal); this.enabled = false; } diff --git a/Dalamud/Hooking/Internal/MinHookHook.cs b/Dalamud/Hooking/Internal/MinHookHook.cs index 0305f3c84..d4889ba11 100644 --- a/Dalamud/Hooking/Internal/MinHookHook.cs +++ b/Dalamud/Hooking/Internal/MinHookHook.cs @@ -50,14 +50,7 @@ internal class MinHookHook : Hook where T : Delegate } /// - public override bool IsEnabled - { - get - { - this.CheckDisposed(); - return this.minHookImpl.Enabled; - } - } + public override bool IsEnabled => !this.IsDisposed && this.minHookImpl.Enabled; /// public override string BackendName => "MinHook"; @@ -84,28 +77,29 @@ internal class MinHookHook : Hook where T : Delegate /// public override void Enable() { - this.CheckDisposed(); - - if (!this.minHookImpl.Enabled) + lock (HookManager.HookEnableSyncRoot) { - lock (HookManager.HookEnableSyncRoot) - { - this.minHookImpl.Enable(); - } + this.CheckDisposed(); + + if (!this.minHookImpl.Enabled) + return; + + this.minHookImpl.Enable(); } } /// public override void Disable() { - this.CheckDisposed(); - - if (this.minHookImpl.Enabled) + lock (HookManager.HookEnableSyncRoot) { - lock (HookManager.HookEnableSyncRoot) - { - this.minHookImpl.Disable(); - } + if (this.IsDisposed) + return; + + if (!this.minHookImpl.Enabled) + return; + + this.minHookImpl.Disable(); } } } diff --git a/Dalamud/Hooking/Internal/ReloadedHook.cs b/Dalamud/Hooking/Internal/ReloadedHook.cs index 2b0a4e9ce..cdd939d19 100644 --- a/Dalamud/Hooking/Internal/ReloadedHook.cs +++ b/Dalamud/Hooking/Internal/ReloadedHook.cs @@ -45,14 +45,7 @@ internal class ReloadedHook : Hook where T : Delegate } /// - public override bool IsEnabled - { - get - { - this.CheckDisposed(); - return this.hookImpl.IsHookEnabled; - } - } + public override bool IsEnabled => !this.IsDisposed && this.hookImpl.IsHookEnabled; /// public override string BackendName => "Reloaded"; @@ -73,10 +66,10 @@ internal class ReloadedHook : Hook where T : Delegate /// public override void Enable() { - this.CheckDisposed(); - lock (HookManager.HookEnableSyncRoot) { + this.CheckDisposed(); + if (!this.hookImpl.IsHookEnabled) this.hookImpl.Enable(); } @@ -85,10 +78,11 @@ internal class ReloadedHook : Hook where T : Delegate /// public override void Disable() { - this.CheckDisposed(); - lock (HookManager.HookEnableSyncRoot) { + if (this.IsDisposed) + return; + if (!this.hookImpl.IsHookActivated) return; diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs index 8191487f4..0d2057b3b 100644 --- a/Dalamud/Interface/Animation/Easing.cs +++ b/Dalamud/Interface/Animation/Easing.cs @@ -85,12 +85,12 @@ public abstract class Easing public TimeSpan Duration { get; set; } /// - /// Gets a value indicating whether or not the animation is running. + /// Gets a value indicating whether the animation is running. /// public bool IsRunning => this.animationTimer.IsRunning; /// - /// Gets a value indicating whether or not the animation is done. + /// Gets a value indicating whether the animation is done. /// public bool IsDone => this.animationTimer.ElapsedMilliseconds > this.Duration.TotalMilliseconds; diff --git a/Dalamud/Interface/DalamudWindowOpenKinds.cs b/Dalamud/Interface/DalamudWindowOpenKinds.cs index 588ff858b..35d2825f7 100644 --- a/Dalamud/Interface/DalamudWindowOpenKinds.cs +++ b/Dalamud/Interface/DalamudWindowOpenKinds.cs @@ -14,16 +14,21 @@ public enum PluginInstallerOpenKind /// Open to the "Installed Plugins" page. /// InstalledPlugins, - + /// /// Open to the "Can be updated" page. /// UpdateablePlugins, /// - /// Open to the "Changelogs" page. + /// Open to the "Plugin Changelogs" page. /// Changelogs, + + /// + /// Open to the "Dalamud Changelogs" page. + /// + DalamudChangelogs, } /// @@ -35,12 +40,12 @@ public enum SettingsOpenKind /// Open to the "General" page. /// General, - + /// /// Open to the "Look & Feel" page. /// LookAndFeel, - + /// /// Open to the "Auto Updates" page. /// diff --git a/Dalamud/Interface/ImGuiFileDialog/DriveListLoader.cs b/Dalamud/Interface/ImGuiFileDialog/DriveListLoader.cs index 487a08132..6998e2ef0 100644 --- a/Dalamud/Interface/ImGuiFileDialog/DriveListLoader.cs +++ b/Dalamud/Interface/ImGuiFileDialog/DriveListLoader.cs @@ -23,7 +23,7 @@ internal class DriveListLoader public IReadOnlyList Drives { get; private set; } /// - /// Gets a value indicating whether or not the loader is loading. + /// Gets a value indicating whether the loader is loading. /// public bool Loading { get; private set; } diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs index 705c0f100..e5b7fc15e 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Dalamud.Utility; + namespace Dalamud.Interface.ImGuiFileDialog; /// @@ -13,11 +15,44 @@ public partial class FileDialog private readonly DriveListLoader driveListLoader = new(); - private List files = new(); - private List filteredFiles = new(); + private readonly List files = []; + private readonly List filteredFiles = []; private SortingField currentSortingField = SortingField.FileName; - private bool[] sortDescending = { false, false, false, false }; + + /// Fired whenever the sorting field changes. + public event Action? SortOrderChanged; + + /// The sorting type of the file selector. + public enum SortingField + { + /// No sorting specified. + None = 0, + + /// Sort for ascending file names in culture-specific order. + FileName = 1, + + /// Sort for ascending file types in culture-specific order. + Type = 2, + + /// Sort for ascending file sizes. + Size = 3, + + /// Sort for ascending last update dates. + Date = 4, + + /// Sort for descending file names in culture-specific order. + FileNameDescending = 5, + + /// Sort for descending file types in culture-specific order. + TypeDescending = 6, + + /// Sort for descending file sizes. + SizeDescending = 7, + + /// Sort for descending last update dates. + DateDescending = 8, + } private enum FileStructType { @@ -25,48 +60,64 @@ public partial class FileDialog Directory, } - private enum SortingField + /// Specify the current and subsequent sort order. + /// The new sort order. None is invalid and will not have any effect. + public void SortFields(SortingField sortingField) { - None, - FileName, - Type, - Size, - Date, + Comparison? sortFunc = sortingField switch + { + SortingField.FileName => SortByFileNameAsc, + SortingField.FileNameDescending => SortByFileNameDesc, + SortingField.Type => SortByTypeAsc, + SortingField.TypeDescending => SortByTypeDesc, + SortingField.Size => SortBySizeAsc, + SortingField.SizeDescending => SortBySizeDesc, + SortingField.Date => SortByDateAsc, + SortingField.DateDescending => SortByDateDesc, + _ => null, + }; + + if (sortFunc is null) + { + return; + } + + this.files.Sort(sortFunc); + this.currentSortingField = sortingField; + this.ApplyFilteringOnFileList(); + this.SortOrderChanged?.InvokeSafely(this.currentSortingField); } - private static string ComposeNewPath(List decomp) + private static string ComposeNewPath(List decomposition) { - // Handle UNC paths (network paths) - if (decomp.Count >= 2 && string.IsNullOrEmpty(decomp[0]) && string.IsNullOrEmpty(decomp[1])) + switch (decomposition.Count) { - var pathParts = new List(decomp); - pathParts.RemoveRange(0, 2); - // Can not access server level or UNC root - if (pathParts.Count <= 1) - { - return string.Empty; - } + // Handle UNC paths (network paths) + case >= 2 when string.IsNullOrEmpty(decomposition[0]) && string.IsNullOrEmpty(decomposition[1]): + var pathParts = new List(decomposition); + pathParts.RemoveRange(0, 2); - return $"\\\\{string.Join('\\', pathParts)}"; + // Can not access server level or UNC root + if (pathParts.Count <= 1) + { + return string.Empty; + } + + return $@"\\{string.Join('\\', pathParts)}"; + case 1: + var drivePath = decomposition[0]; + if (drivePath[^1] != Path.DirectorySeparatorChar) + { // turn C: into C:\ + drivePath += Path.DirectorySeparatorChar; + } + + return drivePath; + default: return Path.Combine(decomposition.ToArray()); } - - if (decomp.Count == 1) - { - var drivePath = decomp[0]; - if (drivePath[^1] != Path.DirectorySeparatorChar) - { // turn C: into C:\ - drivePath += Path.DirectorySeparatorChar; - } - - return drivePath; - } - - return Path.Combine(decomp.ToArray()); } private static FileStruct GetFile(FileInfo file, string path) - { - return new FileStruct + => new() { FileName = file.Name, FilePath = path, @@ -76,11 +127,9 @@ public partial class FileDialog Type = FileStructType.File, Ext = file.Extension.Trim('.'), }; - } private static FileStruct GetDir(DirectoryInfo dir, string path) - { - return new FileStruct + => new() { FileName = dir.Name, FilePath = path, @@ -90,136 +139,191 @@ public partial class FileDialog Type = FileStructType.Directory, Ext = string.Empty, }; - } private static int SortByFileNameDesc(FileStruct a, FileStruct b) { - if (a.FileName[0] == '.' && b.FileName[0] != '.') + switch (a.FileName, b.FileName) { - return 1; + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; } - if (a.FileName[0] != '.' && b.FileName[0] == '.') + if (a.FileName[0] is '.') { - return -1; - } - - if (a.FileName[0] == '.' && b.FileName[0] == '.') - { - if (a.FileName.Length == 1) - { - return -1; - } - - if (b.FileName.Length == 1) + if (b.FileName[0] is not '.') { return 1; } - return -1 * string.Compare(a.FileName[1..], b.FileName[1..]); + if (a.FileName.Length is 1) + { + return -1; + } + + if (b.FileName.Length is 1) + { + return 1; + } + + return -1 * string.Compare(a.FileName[1..], b.FileName[1..], StringComparison.CurrentCulture); + } + + if (b.FileName[0] is '.') + { + return -1; } if (a.Type != b.Type) { - return a.Type == FileStructType.Directory ? 1 : -1; + return a.Type is FileStructType.Directory ? 1 : -1; } - return -1 * string.Compare(a.FileName, b.FileName); + return -string.Compare(a.FileName, b.FileName, StringComparison.CurrentCulture); } private static int SortByFileNameAsc(FileStruct a, FileStruct b) { - if (a.FileName[0] == '.' && b.FileName[0] != '.') + switch (a.FileName, b.FileName) { - return -1; + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; } - if (a.FileName[0] != '.' && b.FileName[0] == '.') + if (a.FileName[0] is '.') { - return 1; - } - - if (a.FileName[0] == '.' && b.FileName[0] == '.') - { - if (a.FileName.Length == 1) - { - return 1; - } - - if (b.FileName.Length == 1) + if (b.FileName[0] is not '.') { return -1; } - return string.Compare(a.FileName[1..], b.FileName[1..]); + if (a.FileName.Length is 1) + { + return 1; + } + + if (b.FileName.Length is 1) + { + return -1; + } + + return string.Compare(a.FileName[1..], b.FileName[1..], StringComparison.CurrentCulture); + } + + if (b.FileName[0] is '.') + { + return 1; } if (a.Type != b.Type) { - return a.Type == FileStructType.Directory ? -1 : 1; + return a.Type is FileStructType.Directory ? -1 : 1; } - return string.Compare(a.FileName, b.FileName); + return string.Compare(a.FileName, b.FileName, StringComparison.CurrentCulture); } private static int SortByTypeDesc(FileStruct a, FileStruct b) { + switch (a.FileName, b.FileName) + { + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; + } + if (a.Type != b.Type) { return (a.Type == FileStructType.Directory) ? 1 : -1; } - return string.Compare(a.Ext, b.Ext); + return string.Compare(a.Ext, b.Ext, StringComparison.CurrentCulture); } private static int SortByTypeAsc(FileStruct a, FileStruct b) { + switch (a.FileName, b.FileName) + { + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; + } + if (a.Type != b.Type) { return (a.Type == FileStructType.Directory) ? -1 : 1; } - return -1 * string.Compare(a.Ext, b.Ext); + return -string.Compare(a.Ext, b.Ext, StringComparison.CurrentCulture); } private static int SortBySizeDesc(FileStruct a, FileStruct b) { - if (a.Type != b.Type) + switch (a.FileName, b.FileName) { - return (a.Type == FileStructType.Directory) ? 1 : -1; + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; } - return (a.FileSize > b.FileSize) ? 1 : -1; + if (a.Type != b.Type) + { + return (a.Type is FileStructType.Directory) ? 1 : -1; + } + + return a.FileSize.CompareTo(b.FileSize); } private static int SortBySizeAsc(FileStruct a, FileStruct b) { - if (a.Type != b.Type) + switch (a.FileName, b.FileName) { - return (a.Type == FileStructType.Directory) ? -1 : 1; + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; } - return (a.FileSize > b.FileSize) ? -1 : 1; + if (a.Type != b.Type) + { + return (a.Type is FileStructType.Directory) ? -1 : 1; + } + + return -a.FileSize.CompareTo(b.FileSize); } private static int SortByDateDesc(FileStruct a, FileStruct b) { + switch (a.FileName, b.FileName) + { + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; + } + if (a.Type != b.Type) { return (a.Type == FileStructType.Directory) ? 1 : -1; } - return string.Compare(a.FileModifiedDate, b.FileModifiedDate); + return string.Compare(a.FileModifiedDate, b.FileModifiedDate, StringComparison.CurrentCulture); } private static int SortByDateAsc(FileStruct a, FileStruct b) { + switch (a.FileName, b.FileName) + { + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; + } + if (a.Type != b.Type) { return (a.Type == FileStructType.Directory) ? -1 : 1; } - return -1 * string.Compare(a.FileModifiedDate, b.FileModifiedDate); + return -string.Compare(a.FileModifiedDate, b.FileModifiedDate, StringComparison.CurrentCulture); } private bool CreateDir(string dirPath) @@ -336,52 +440,17 @@ public partial class FileDialog this.quickAccess.Add(new SideBarItem("Videos", Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), FontAwesomeIcon.Video)); } - private void SortFields(SortingField sortingField, bool canChangeOrder = false) - { - switch (sortingField) + private SortingField GetNewSorting(int column) + => column switch { - case SortingField.FileName: - if (canChangeOrder && sortingField == this.currentSortingField) - { - this.sortDescending[0] = !this.sortDescending[0]; - } - - this.files.Sort(this.sortDescending[0] ? SortByFileNameDesc : SortByFileNameAsc); - break; - - case SortingField.Type: - if (canChangeOrder && sortingField == this.currentSortingField) - { - this.sortDescending[1] = !this.sortDescending[1]; - } - - this.files.Sort(this.sortDescending[1] ? SortByTypeDesc : SortByTypeAsc); - break; - - case SortingField.Size: - if (canChangeOrder && sortingField == this.currentSortingField) - { - this.sortDescending[2] = !this.sortDescending[2]; - } - - this.files.Sort(this.sortDescending[2] ? SortBySizeDesc : SortBySizeAsc); - break; - - case SortingField.Date: - if (canChangeOrder && sortingField == this.currentSortingField) - { - this.sortDescending[3] = !this.sortDescending[3]; - } - - this.files.Sort(this.sortDescending[3] ? SortByDateDesc : SortByDateAsc); - break; - } - - if (sortingField != SortingField.None) - { - this.currentSortingField = sortingField; - } - - this.ApplyFilteringOnFileList(); - } + 0 when this.currentSortingField is SortingField.FileName => SortingField.FileNameDescending, + 0 => SortingField.FileName, + 1 when this.currentSortingField is SortingField.Type => SortingField.TypeDescending, + 1 => SortingField.Type, + 2 when this.currentSortingField is SortingField.Size => SortingField.SizeDescending, + 2 => SortingField.Size, + 3 when this.currentSortingField is SortingField.Date => SortingField.DateDescending, + 3 => SortingField.Date, + _ => SortingField.None, + }; } diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs index ed45ec7fc..fa74b424a 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs @@ -374,22 +374,8 @@ public partial class FileDialog ImGui.PopID(); if (ImGui.IsItemClicked()) { - if (column == 0) - { - this.SortFields(SortingField.FileName, true); - } - else if (column == 1) - { - this.SortFields(SortingField.Type, true); - } - else if (column == 2) - { - this.SortFields(SortingField.Size, true); - } - else - { - this.SortFields(SortingField.Date, true); - } + var sorting = this.GetNewSorting(column); + this.SortFields(sorting); } } diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs index cd4bbb4ef..ee12e7424 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs @@ -9,13 +9,21 @@ namespace Dalamud.Interface.ImGuiFileDialog; /// public class FileDialogManager { + /// Gets or sets a function that returns the desired default sort order in the file dialog. + public Func? GetDefaultSortOrder { get; set; } + + /// Gets or sets an action to invoke when a file dialog changes its sort order. + public Action? SetDefaultSortOrder { get; set; } + #pragma warning disable SA1401 - /// Additional quick access items for the side bar. - public readonly List<(string Name, string Path, FontAwesomeIcon Icon, int Position)> CustomSideBarItems = new(); +#pragma warning disable SA1201 + /// Additional quick access items for the sidebar. + public readonly List<(string Name, string Path, FontAwesomeIcon Icon, int Position)> CustomSideBarItems = []; /// Additional flags with which to draw the window. public ImGuiWindowFlags AddedWindowFlags = ImGuiWindowFlags.None; #pragma warning restore SA1401 +#pragma warning restore SA1201 private FileDialog? dialog; private Action? callback; @@ -189,10 +197,41 @@ public class FileDialogManager this.callback = callback as Action; } + if (this.dialog is not null) + { + this.dialog.SortOrderChanged -= this.OnSortOrderChange; + } + this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags); + if (this.GetDefaultSortOrder is not null) + { + try + { + var order = this.GetDefaultSortOrder(); + this.dialog.SortFields(order); + } + catch + { + // ignored. + } + } + + this.dialog.SortOrderChanged += this.OnSortOrderChange; this.dialog.WindowFlags |= this.AddedWindowFlags; foreach (var (name, location, icon, position) in this.CustomSideBarItems) this.dialog.SetQuickAccess(name, location, icon, position); this.dialog.Show(); } + + private void OnSortOrderChange(FileDialog.SortingField sortOrder) + { + try + { + this.SetDefaultSortOrder?.Invoke(sortOrder); + } + catch + { + // ignored. + } + } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index dfecd55fe..1afc1262b 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -14,8 +14,10 @@ internal sealed partial class ActiveNotification /// Draws this notification. /// The maximum width of the notification window. /// The offset from the bottom. + /// Where notifications are anchored to on the screen. + /// Direction of the screen which we are snapping to. /// The height of the notification. - public float Draw(float width, float offsetY) + public float Draw(float width, float offsetY, Vector2 anchorPosition, NotificationSnapDirection snapDirection) { var opacity = Math.Clamp( @@ -34,8 +36,8 @@ internal sealed partial class ActiveNotification (NotificationConstants.ScaledWindowPadding * 2); var viewport = ImGuiHelpers.MainViewport; - var viewportPos = viewport.WorkPos; var viewportSize = viewport.WorkSize; + var viewportPos = viewport.Pos; ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); @@ -51,13 +53,78 @@ internal sealed partial class ActiveNotification NotificationConstants.BackgroundOpacity)); } + Vector2 topLeft; + Vector2 pivot; + if (snapDirection is NotificationSnapDirection.Top or NotificationSnapDirection.Bottom) + { + // Top or bottom + var xPos = (viewportSize.X - width) * anchorPosition.X; + xPos = Math.Max(NotificationConstants.ScaledViewportEdgeMargin, Math.Min(viewportSize.X - width - NotificationConstants.ScaledViewportEdgeMargin, xPos)); + + if (snapDirection == NotificationSnapDirection.Top) + { + // Top + var yPos = NotificationConstants.ScaledViewportEdgeMargin - offsetY; + topLeft = new Vector2(xPos, yPos); + pivot = new(0, 0); + } + else + { + // Bottom + var yPos = viewportSize.Y - offsetY - NotificationConstants.ScaledViewportEdgeMargin; + topLeft = new Vector2(xPos, yPos); + pivot = new(0, 1); + } + } + else + { + // Left or Right + var yPos = (viewportSize.Y * anchorPosition.Y) - offsetY; + yPos = Math.Max( + NotificationConstants.ScaledViewportEdgeMargin, + Math.Min(viewportSize.Y - offsetY - NotificationConstants.ScaledViewportEdgeMargin, yPos)); + + if (snapDirection == NotificationSnapDirection.Left) + { + // Left + var xPos = NotificationConstants.ScaledViewportEdgeMargin; + + if (anchorPosition.Y > 0.5f) + { + // Bottom + topLeft = new Vector2(xPos, yPos); + pivot = new(0, 1); + } + else + { + // Top + topLeft = new Vector2(xPos, yPos); + pivot = new(0, 0); + } + } + else + { + // Right + var xPos = viewportSize.X - width - NotificationConstants.ScaledViewportEdgeMargin; + + if (anchorPosition.Y > 0.5f) + { + topLeft = new Vector2(xPos, yPos); + pivot = new(0, 1); + } + else + { + topLeft = new Vector2(xPos, yPos); + pivot = new(0, 0); + } + } + } + ImGuiHelpers.ForceNextWindowMainViewport(); ImGui.SetNextWindowPos( - (viewportPos + viewportSize) - - new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - - new Vector2(0, offsetY), + topLeft + viewportPos, ImGuiCond.Always, - Vector2.One); + pivot); ImGui.SetNextWindowSizeConstraints( new(width, actionWindowHeight), new( @@ -145,7 +212,7 @@ internal sealed partial class ActiveNotification ImGui.PopStyleColor(); ImGui.PopStyleVar(3); - return windowSize.Y; + return NotificationManager.ShouldScrollDownwards(anchorPosition) ? -windowSize.Y : windowSize.Y; } /// Calculates the effective expiry, taking ImGui window state into account. diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index 8b7ce7bfa..b79855a6b 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -54,6 +54,11 @@ internal static class NotificationConstants /// public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; + /// + /// The ratio of the screen at which the notification window will snap to the top or bottom of the screen. + /// + public const float NotificationTopBottomSnapMargin = 0.08f; + /// Default duration of the notification. public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(7); diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index 67cd0ca19..340763a55 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -1,7 +1,9 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using System.Numerics; using Dalamud.Bindings.ImGui; +using Dalamud.Configuration.Internal; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ManagedFontAtlas; @@ -21,9 +23,14 @@ internal class NotificationManager : INotificationManager, IInternalDisposableSe [ServiceManager.ServiceDependency] private readonly GameGui gameGui = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly DalamudConfiguration configuration = Service.Get(); + private readonly List notifications = new(); private readonly ConcurrentBag pendingNotifications = new(); + private NotificationPositionChooser? positionChooser; + [ServiceManager.ServiceConstructor] private NotificationManager(FontAtlasFactory fontAtlasFactory) { @@ -47,6 +54,48 @@ internal class NotificationManager : INotificationManager, IInternalDisposableSe /// Gets the private atlas for use with notification windows. private IFontAtlas PrivateAtlas { get; } + /// + /// Calculate the width to be used to draw notifications. + /// + /// The width. + public static float CalculateNotificationWidth() + { + var viewportSize = ImGuiHelpers.MainViewport.WorkSize; + var width = ImGui.CalcTextSize(NotificationConstants.NotificationWidthMeasurementString).X; + width += NotificationConstants.ScaledWindowPadding * 3; + width += NotificationConstants.ScaledIconSize; + return Math.Min(width, viewportSize.X * NotificationConstants.MaxNotificationWindowWidthWrtMainViewportWidth); + } + + /// + /// Check if notifications should scroll downwards on the screen, based on the anchor position. + /// + /// Where notifications are anchored to. + /// A value indicating wether notifications should scroll downwards. + public static bool ShouldScrollDownwards(Vector2 anchorPosition) + { + return anchorPosition.Y < 0.5f; + } + + /// + /// Choose the snap position for a notification based on the anchor position. + /// + /// Where notifications are anchored to. + /// The snap position. + public static NotificationSnapDirection ChooseSnapDirection(Vector2 anchorPosition) + { + if (anchorPosition.Y <= NotificationConstants.NotificationTopBottomSnapMargin) + return NotificationSnapDirection.Top; + + if (anchorPosition.Y >= 1f - NotificationConstants.NotificationTopBottomSnapMargin) + return NotificationSnapDirection.Bottom; + + if (anchorPosition.X <= 0.5f) + return NotificationSnapDirection.Left; + + return NotificationSnapDirection.Right; + } + /// public void DisposeService() { @@ -97,25 +146,38 @@ internal class NotificationManager : INotificationManager, IInternalDisposableSe /// Draw all currently queued notifications. public void Draw() { - var viewportSize = ImGuiHelpers.MainViewport.WorkSize; var height = 0f; var uiHidden = this.gameGui.GameUiHidden; while (this.pendingNotifications.TryTake(out var newNotification)) this.notifications.Add(newNotification); - var width = ImGui.CalcTextSize(NotificationConstants.NotificationWidthMeasurementString).X; - width += NotificationConstants.ScaledWindowPadding * 3; - width += NotificationConstants.ScaledIconSize; - width = Math.Min(width, viewportSize.X * NotificationConstants.MaxNotificationWindowWidthWrtMainViewportWidth); + var width = CalculateNotificationWidth(); this.notifications.RemoveAll(static x => x.UpdateOrDisposeInternal()); + + var scrollsDownwards = ShouldScrollDownwards(this.configuration.NotificationAnchorPosition); + var snapDirection = ChooseSnapDirection(this.configuration.NotificationAnchorPosition); + foreach (var tn in this.notifications) { if (uiHidden && tn.RespectUiHidden) continue; - height += tn.Draw(width, height) + NotificationConstants.ScaledWindowGap; + + height += tn.Draw(width, height, this.configuration.NotificationAnchorPosition, snapDirection); + height += scrollsDownwards ? -NotificationConstants.ScaledWindowGap : NotificationConstants.ScaledWindowGap; } + + this.positionChooser?.Draw(); + } + + /// + /// Starts the position chooser for notifications. Will block the UI until the user makes a selection. + /// + public void StartPositionChooser() + { + this.positionChooser = new NotificationPositionChooser(this.configuration); + this.positionChooser.SelectionMade += () => { this.positionChooser = null; }; } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationPositionChooser.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationPositionChooser.cs new file mode 100644 index 000000000..d92778ad5 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationPositionChooser.cs @@ -0,0 +1,225 @@ +using System.Numerics; + +using Dalamud.Bindings.ImGui; +using Dalamud.Configuration.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; + +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// +/// Class responsible for drawing UI that lets users choose the position of notifications. +/// +internal class NotificationPositionChooser +{ + private readonly DalamudConfiguration configuration; + private readonly Vector2 previousAnchorPosition; + + private Vector2 currentAnchorPosition; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration we are reading or writing from. + public NotificationPositionChooser(DalamudConfiguration configuration) + { + this.configuration = configuration; + this.previousAnchorPosition = configuration.NotificationAnchorPosition; + } + + /// + /// Gets or sets an action that is invoked when the user makes a selection. + /// + public event Action? SelectionMade; + + /// + /// Draw the chooser UI. + /// + public void Draw() + { + using var style1 = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 0f); + using var style2 = ImRaii.PushStyle(ImGuiStyleVar.WindowBorderSize, 0f); + using var color = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0, 0, 0, 0)); + + var viewport = ImGuiHelpers.MainViewport; + var viewportSize = viewport.Size; + var viewportPos = viewport.Pos; + + ImGui.SetNextWindowFocus(); + ImGui.SetNextWindowPos(viewportPos); + ImGui.SetNextWindowSize(viewportSize); + ImGuiHelpers.ForceNextWindowMainViewport(); + + ImGui.SetNextWindowBgAlpha(0.6f); + + ImGui.Begin( + "###NotificationPositionChooser", + ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNav); + + var adjustedMousePos = ImGui.GetMousePos() - viewportPos; + var mousePosUnit = adjustedMousePos / viewportSize; + + // Store the offset as a Vector2 + this.currentAnchorPosition = mousePosUnit; + + DrawPreview(this.previousAnchorPosition, 0.3f); + DrawPreview(this.currentAnchorPosition, 1f); + + if (ImGui.IsMouseClicked(ImGuiMouseButton.Right)) + { + this.SelectionMade?.Invoke(); + } + else if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + { + this.configuration.NotificationAnchorPosition = this.currentAnchorPosition; + this.configuration.QueueSave(); + + this.SelectionMade?.Invoke(); + } + + // In the middle of the screen, draw some instructions + string[] instructions = ["Drag to move the notifications to where you would like them to appear.", + "Click to select the position.", + "Right-click to close without making changes."]; + + var dl = ImGui.GetWindowDrawList(); + for (var i = 0; i < instructions.Length; i++) + { + var instruction = instructions[i]; + var instructionSize = ImGui.CalcTextSize(instruction); + var instructionPos = new Vector2( + ImGuiHelpers.MainViewport.Size.X / 2 - instructionSize.X / 2, + ImGuiHelpers.MainViewport.Size.Y / 2 - instructionSize.Y / 2 + i * instructionSize.Y); + instructionPos += viewportPos; + dl.AddText(instructionPos, 0xFFFFFFFF, instruction); + } + + ImGui.End(); + } + + private static void DrawPreview(Vector2 anchorPosition, float borderAlpha) + { + var dl = ImGui.GetWindowDrawList(); + var width = NotificationManager.CalculateNotificationWidth(); + var height = 100f * ImGuiHelpers.GlobalScale; + var smallBoxHeight = height * 0.4f; + var edgeMargin = NotificationConstants.ScaledViewportEdgeMargin; + var spacing = 10f * ImGuiHelpers.GlobalScale; + + var viewport = ImGuiHelpers.MainViewport; + var viewportSize = viewport.Size; + var viewportPos = viewport.Pos; + var borderColor = ImGui.ColorConvertFloat4ToU32(new(1f, 1f, 1f, borderAlpha)); + var borderThickness = 4.0f * ImGuiHelpers.GlobalScale; + var borderRounding = 4.0f * ImGuiHelpers.GlobalScale; + var backgroundColor = new Vector4(0, 0, 0, 0.5f); // Semi-transparent black + + // Calculate positions based on the snap position + Vector2 topLeft, bottomRight, smallTopLeft, smallBottomRight; + + var snapPos = NotificationManager.ChooseSnapDirection(anchorPosition); + if (snapPos is NotificationSnapDirection.Top or NotificationSnapDirection.Bottom) + { + // Calculate X position - same logic for top and bottom + var xPos = (viewportSize.X - width) * anchorPosition.X; + xPos = Math.Max(edgeMargin, Math.Min(viewportSize.X - width - edgeMargin, xPos)); + + if (snapPos == NotificationSnapDirection.Top) + { + // For top position: big box at top, small box below it + var yPos = edgeMargin; + topLeft = new Vector2(xPos, yPos); + bottomRight = new Vector2(xPos + width, yPos + height); + + smallTopLeft = new Vector2(xPos, yPos + height + spacing); + smallBottomRight = new Vector2(xPos + width, yPos + height + spacing + smallBoxHeight); + } + else + { + // For bottom position: big box at bottom, small box above it + var yPos = viewportSize.Y - height - edgeMargin; + topLeft = new Vector2(xPos, yPos); + bottomRight = new Vector2(xPos + width, yPos + height); + + smallTopLeft = new Vector2(xPos, yPos - smallBoxHeight - spacing); + smallBottomRight = new Vector2(xPos + width, yPos - spacing); + } + } + else + { + // For left and right positions, boxes are still stacked vertically (one above the other) + // Only the horizontal position changes + + // Calculate Y position based on unit offset - used for both left and right positions + var yPos = (viewportSize.Y - height) * anchorPosition.Y; + yPos = Math.Max(edgeMargin, Math.Min(viewportSize.Y - height - edgeMargin, yPos)); + + if (snapPos == NotificationSnapDirection.Left) + { + // For left position: boxes are at the left edge of the screen + var xPos = edgeMargin; + + if (anchorPosition.Y > 0.5f) + { + // Small box on top + smallTopLeft = new Vector2(xPos, yPos - smallBoxHeight - spacing); + smallBottomRight = new Vector2(xPos + width, yPos - spacing); + + // Big box below + topLeft = new Vector2(xPos, yPos); + bottomRight = new Vector2(xPos + width, yPos + height); + } + else + { + // Big box on top + topLeft = new Vector2(xPos, yPos); + bottomRight = new Vector2(xPos + width, yPos + height); + + // Small box below + smallTopLeft = new Vector2(xPos, yPos + height + spacing); + smallBottomRight = new Vector2(xPos + width, yPos + height + spacing + smallBoxHeight); + } + } + else + { + // For right position: boxes are at the right edge of the screen + var xPos = viewportSize.X - width - edgeMargin; + + if (anchorPosition.Y > 0.5f) + { + // Small box on top + smallTopLeft = new Vector2(xPos, yPos - smallBoxHeight - spacing); + smallBottomRight = new Vector2(xPos + width, yPos - spacing); + + // Big box below + topLeft = new Vector2(xPos, yPos); + bottomRight = new Vector2(xPos + width, yPos + height); + } + else + { + // Big box on top + topLeft = new Vector2(xPos, yPos); + bottomRight = new Vector2(xPos + width, yPos + height); + + // Small box below + smallTopLeft = new Vector2(xPos, yPos + height + spacing); + smallBottomRight = new Vector2(xPos + width, yPos + height + spacing + smallBoxHeight); + } + } + } + + topLeft += viewportPos; + bottomRight += viewportPos; + smallTopLeft += viewportPos; + smallBottomRight += viewportPos; + + // Draw the big box + dl.AddRectFilled(topLeft, bottomRight, ImGui.ColorConvertFloat4ToU32(backgroundColor), borderRounding, ImDrawFlags.RoundCornersAll); + dl.AddRect(topLeft, bottomRight, borderColor, borderRounding, ImDrawFlags.RoundCornersAll, borderThickness); + + // Draw the small box + dl.AddRectFilled(smallTopLeft, smallBottomRight, ImGui.ColorConvertFloat4ToU32(backgroundColor), borderRounding, ImDrawFlags.RoundCornersAll); + dl.AddRect(smallTopLeft, smallBottomRight, borderColor, borderRounding, ImDrawFlags.RoundCornersAll, borderThickness); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationSnapDirection.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationSnapDirection.cs new file mode 100644 index 000000000..1666e7a8c --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationSnapDirection.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// +/// Where notifications should snap to on the screen when they are shown. +/// +public enum NotificationSnapDirection +{ + /// + /// Snap to the top of the screen. + /// + Top, + + /// + /// Snap to the bottom of the screen. + /// + Bottom, + + /// + /// Snap to the left of the screen. + /// + Left, + + /// + /// Snap to the right of the screen. + /// + Right, +} diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index d32b842e1..b1fdb5232 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -146,6 +146,11 @@ internal class DalamudCommands : IServiceType "DalamudCopyLogHelp", "Copy the dalamud.log file to your clipboard."), }); + // Add the new command handler for toggling multi-monitor option + commandManager.AddHandler("/xltogglemultimonitor", new CommandInfo(this.OnToggleMultiMonitorCommand) + { + HelpMessage = Loc.Localize("DalamudToggleMultiMonitorHelp", "Toggle multi-monitor windows."), + }); } private void OnUnloadCommand(string command, string arguments) @@ -387,4 +392,19 @@ internal class DalamudCommands : IServiceType : Loc.Localize("DalamudLogCopyFailure", "Could not copy log file to clipboard."); chatGui.Print(message); } + + private void OnToggleMultiMonitorCommand(string command, string arguments) + { + var configuration = Service.Get(); + var chatGui = Service.Get(); + + configuration.IsDisableViewport = !configuration.IsDisableViewport; + configuration.QueueSave(); + + var message = configuration.IsDisableViewport + ? Loc.Localize("DalamudMultiMonitorDisabled", "Multi-monitor windows disabled.") + : Loc.Localize("DalamudMultiMonitorEnabled", "Multi-monitor windows enabled."); + + chatGui.Print(message); + } } diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 55e705c47..7b5661bc5 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -11,13 +11,11 @@ using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImPlot; using Dalamud.Configuration.Internal; using Dalamud.Console; -using Dalamud.Data; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Gui; -using Dalamud.Game.Internal; using Dalamud.Hooking; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; @@ -305,8 +303,14 @@ internal class DalamudInterface : IInternalDisposableService /// /// Opens the . /// - public void OpenLogWindow() + /// The filter to set, if not null. + public void OpenLogWindow(string? textFilter = "") { + if (textFilter != null) + { + this.consoleWindow.TextFilter = textFilter; + } + this.consoleWindow.IsOpen = true; this.consoleWindow.BringToFront(); } @@ -518,7 +522,7 @@ internal class DalamudInterface : IInternalDisposableService /// /// Toggle the screen darkening effect used for the credits. /// - /// Whether or not to turn the effect on. + /// Whether to turn the effect on. public void SetCreditsDarkeningAnimation(bool status) { this.isCreditsDarkening = status; @@ -713,19 +717,6 @@ internal class DalamudInterface : IInternalDisposableService this.dalamud.StartInfo.LogName); } - var antiDebug = Service.Get(); - if (ImGui.MenuItem("Disable Debugging Protections", (byte*)null, antiDebug.IsEnabled)) - { - var newEnabled = !antiDebug.IsEnabled; - if (newEnabled) - antiDebug.Enable(); - else - antiDebug.Disable(); - - this.configuration.IsAntiAntiDebugEnabled = newEnabled; - this.configuration.QueueSave(); - } - ImGui.Separator(); if (ImGui.MenuItem("Open Data window")) @@ -1012,7 +1003,7 @@ internal class DalamudInterface : IInternalDisposableService if (ImGui.MenuItem("Scan dev plugins")) { - pluginManager.ScanDevPlugins(); + _ = pluginManager.ScanDevPluginsAsync(); } ImGui.Separator(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index d392403be..d0a02d9d7 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -34,13 +34,14 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; using Dalamud.Utility.Timing; -using FFXIVClientStructs.FFXIV.Client.Graphics.Environment; using JetBrains.Annotations; using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; using static TerraFX.Interop.Windows.Windows; +using CSFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework; + using DWMWINDOWATTRIBUTE = Windows.Win32.Graphics.Dwm.DWMWINDOWATTRIBUTE; // general dev notes, here because it's easiest @@ -198,7 +199,7 @@ internal partial class InterfaceManager : IInternalDisposableService public IImGuiBackend? Backend => this.backend; /// - /// Gets or sets a value indicating whether or not the game's cursor should be overridden with the ImGui cursor. + /// Gets or sets a value indicating whether the game's cursor should be overridden with the ImGui cursor. /// public bool OverrideGameCursor { @@ -217,7 +218,7 @@ internal partial class InterfaceManager : IInternalDisposableService public bool IsReady => this.backend != null; /// - /// Gets or sets a value indicating whether or not Draw events should be dispatched. + /// Gets or sets a value indicating whether Draw events should be dispatched. /// public bool IsDispatchingEvents { get; set; } = true; @@ -515,7 +516,9 @@ internal partial class InterfaceManager : IInternalDisposableService // Some graphics drivers seem to consider the game's shader cache as invalid if we hook too early. // The game loads shader packages on the file thread and then compiles them. It will show the logo once it is done. // This is a workaround, but it fixes an issue where the game would take a very long time to get to the title screen. - if (EnvManager.Instance() == null) + // NetworkModuleProxy is set up after lua scripts are loaded (EventFramework.LoadState >= 5), which can only happen + // after the shaders are compiled (if necessary) and loaded. AgentLobby.Update doesn't do much until this condition is met. + if (CSFramework.Instance()->GetNetworkModuleProxy() == null) return; this.SetupHooks(Service.Get(), Service.Get()); diff --git a/Dalamud/Interface/Internal/StaThreadService.cs b/Dalamud/Interface/Internal/StaThreadService.cs new file mode 100644 index 000000000..87e003288 --- /dev/null +++ b/Dalamud/Interface/Internal/StaThreadService.cs @@ -0,0 +1,282 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Utility; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.Internal; + +/// Dedicated thread for OLE operations, and possibly more native thread-serialized operations. +[ServiceManager.EarlyLoadedService] +internal partial class StaThreadService : IInternalDisposableService +{ + private readonly CancellationTokenSource cancellationTokenSource = new(); + private readonly Thread thread; + private readonly ThreadBoundTaskScheduler taskScheduler; + private readonly TaskFactory taskFactory; + + private readonly TaskCompletionSource messageReceiverHwndTask = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + [ServiceManager.ServiceConstructor] + private StaThreadService() + { + try + { + this.thread = new(this.OleThreadBody); + this.thread.SetApartmentState(ApartmentState.STA); + + this.taskScheduler = new(this.thread); + this.taskScheduler.TaskQueued += this.TaskSchedulerOnTaskQueued; + this.taskFactory = new( + this.cancellationTokenSource.Token, + TaskCreationOptions.None, + TaskContinuationOptions.None, + this.taskScheduler); + + this.thread.Start(); + this.messageReceiverHwndTask.Task.Wait(); + } + catch (Exception e) + { + this.cancellationTokenSource.Cancel(); + this.messageReceiverHwndTask.SetException(e); + throw; + } + } + + /// Gets all the available clipboard formats. + public IReadOnlySet AvailableClipboardFormats { get; private set; } = ImmutableSortedSet.Empty; + + /// Places a pointer to a specific data object onto the clipboard. This makes the data object accessible + /// to the function. + /// Pointer to the interface on the data object from which the data to + /// be placed on the clipboard can be obtained. This parameter can be NULL; in which case the clipboard is emptied. + /// + /// This function returns on success. + [LibraryImport("ole32.dll")] + public static unsafe partial int OleSetClipboard(IDataObject* pdo); + + /// + public static unsafe void OleSetClipboard(ComPtr pdo) => + Marshal.ThrowExceptionForHR(OleSetClipboard(pdo.Get())); + + /// Retrieves a data object that you can use to access the contents of the clipboard. + /// Address of pointer variable that receives the interface pointer to + /// the clipboard data object. + /// This function returns on success. + [LibraryImport("ole32.dll")] + public static unsafe partial int OleGetClipboard(IDataObject** pdo); + + /// + public static unsafe ComPtr OleGetClipboard() + { + var pdo = default(ComPtr); + Marshal.ThrowExceptionForHR(OleGetClipboard(pdo.GetAddressOf())); + return pdo; + } + + /// Calls the appropriate method or function to release the specified storage medium. + /// Address of to release. + [LibraryImport("ole32.dll")] + public static unsafe partial void ReleaseStgMedium(STGMEDIUM* stgm); + + /// + public static unsafe void ReleaseStgMedium(ref STGMEDIUM stgm) + { + fixed (STGMEDIUM* pstgm = &stgm) + ReleaseStgMedium(pstgm); + } + + /// + void IInternalDisposableService.DisposeService() + { + this.cancellationTokenSource.Cancel(); + if (this.messageReceiverHwndTask.Task.IsCompletedSuccessfully) + SendMessageW(this.messageReceiverHwndTask.Task.Result, WM.WM_CLOSE, 0, 0); + + this.thread.Join(); + } + + /// Runs a given delegate in the messaging thread. + /// Delegate to run. + /// Optional cancellation token. + /// A representating the state of the operation. + public async Task Run(Action action, CancellationToken cancellationToken = default) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource( + this.cancellationTokenSource.Token, + cancellationToken); + await this.taskFactory.StartNew(action, cancellationToken).ConfigureAwait(true); + } + + /// Runs a given delegate in the messaging thread. + /// Type of the return value. + /// Delegate to run. + /// Optional cancellation token. + /// A representating the state of the operation. + public async Task Run(Func func, CancellationToken cancellationToken = default) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource( + this.cancellationTokenSource.Token, + cancellationToken); + return await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true); + } + + /// Runs a given delegate in the messaging thread. + /// Delegate to run. + /// Optional cancellation token. + /// A representating the state of the operation. + public async Task Run(Func func, CancellationToken cancellationToken = default) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource( + this.cancellationTokenSource.Token, + cancellationToken); + await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true); + } + + /// Runs a given delegate in the messaging thread. + /// Type of the return value. + /// Delegate to run. + /// Optional cancellation token. + /// A representating the state of the operation. + public async Task Run(Func> func, CancellationToken cancellationToken = default) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource( + this.cancellationTokenSource.Token, + cancellationToken); + return await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true); + } + + [LibraryImport("ole32.dll")] + private static partial int OleInitialize(nint reserved); + + [LibraryImport("ole32.dll")] + private static partial void OleUninitialize(); + + [LibraryImport("ole32.dll")] + private static partial int OleFlushClipboard(); + + private void TaskSchedulerOnTaskQueued() => + PostMessageW(this.messageReceiverHwndTask.Task.Result, WM.WM_NULL, 0, 0); + + private void UpdateAvailableClipboardFormats(HWND hWnd) + { + if (!OpenClipboard(hWnd)) + { + this.AvailableClipboardFormats = ImmutableSortedSet.Empty; + return; + } + + var formats = new SortedSet(); + for (var cf = EnumClipboardFormats(0); cf != 0; cf = EnumClipboardFormats(cf)) + formats.Add(cf); + this.AvailableClipboardFormats = formats; + CloseClipboard(); + } + + private LRESULT MessageReceiverWndProc(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam) + { + this.taskScheduler.Run(); + + switch (uMsg) + { + case WM.WM_CLIPBOARDUPDATE: + this.UpdateAvailableClipboardFormats(hWnd); + break; + + case WM.WM_DESTROY: + PostQuitMessage(0); + return 0; + } + + return DefWindowProcW(hWnd, uMsg, wParam, lParam); + } + + private unsafe void OleThreadBody() + { + var hInstance = (HINSTANCE)Marshal.GetHINSTANCE(typeof(StaThreadService).Module); + ushort wndClassAtom = 0; + var gch = GCHandle.Alloc(this); + try + { + ((HRESULT)OleInitialize(0)).ThrowOnError(); + + fixed (char* name = typeof(StaThreadService).FullName!) + { + var wndClass = new WNDCLASSEXW + { + cbSize = (uint)sizeof(WNDCLASSEXW), + lpfnWndProc = &MessageReceiverWndProcStatic, + hInstance = hInstance, + hbrBackground = (HBRUSH)(COLOR.COLOR_BACKGROUND + 1), + lpszClassName = (ushort*)name, + }; + + wndClassAtom = RegisterClassExW(&wndClass); + if (wndClassAtom == 0) + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + + this.messageReceiverHwndTask.SetResult( + CreateWindowExW( + 0, + (ushort*)wndClassAtom, + (ushort*)name, + 0, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + default, + default, + hInstance, + (void*)GCHandle.ToIntPtr(gch))); + + [UnmanagedCallersOnly] + static LRESULT MessageReceiverWndProcStatic(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam) + { + nint gchn; + if (uMsg == WM.WM_NCCREATE) + { + gchn = (nint)((CREATESTRUCTW*)lParam)->lpCreateParams; + SetWindowLongPtrW(hWnd, GWLP.GWLP_USERDATA, gchn); + } + else + { + gchn = GetWindowLongPtrW(hWnd, GWLP.GWLP_USERDATA); + } + + if (gchn == 0) + return DefWindowProcW(hWnd, uMsg, wParam, lParam); + + return ((StaThreadService)GCHandle.FromIntPtr(gchn).Target!) + .MessageReceiverWndProc(hWnd, uMsg, wParam, lParam); + } + } + + AddClipboardFormatListener(this.messageReceiverHwndTask.Task.Result); + this.UpdateAvailableClipboardFormats(this.messageReceiverHwndTask.Task.Result); + + for (MSG msg; GetMessageW(&msg, default, 0, 0);) + { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + catch (Exception e) + { + gch.Free(); + _ = OleFlushClipboard(); + OleUninitialize(); + if (wndClassAtom != 0) + UnregisterClassW((ushort*)wndClassAtom, hInstance); + this.messageReceiverHwndTask.TrySetException(e); + } + } +} diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 9410bb371..b700a1610 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -429,17 +429,17 @@ internal unsafe class UiDebug ImGui.SameLine(); Service.Get().Draw(textInputComponent->UnkText02); - ImGui.Text("Text3: "); + ImGui.Text("AvailableLines: "); ImGui.SameLine(); - Service.Get().Draw(textInputComponent->UnkText03); + Service.Get().Draw(textInputComponent->AvailableLines); - ImGui.Text("Text4: "); + ImGui.Text("HighlightedAutoTranslateOptionColorPrefix: "); ImGui.SameLine(); - Service.Get().Draw(textInputComponent->UnkText04); + Service.Get().Draw(textInputComponent->HighlightedAutoTranslateOptionColorPrefix); - ImGui.Text("Text5: "); + ImGui.Text("HighlightedAutoTranslateOptionColorSuffix: "); ImGui.SameLine(); - Service.Get().Draw(textInputComponent->UnkText05); + Service.Get().Draw(textInputComponent->HighlightedAutoTranslateOptionColorSuffix); break; } diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs index 7b7902368..caba115ed 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs @@ -58,7 +58,13 @@ internal unsafe class ComponentNodeTree : ResNodeTree /// private protected override void PrintChildNodes() { - base.PrintChildNodes(); + var prevNode = this.CompNode->Component->UldManager.RootNode; + while (prevNode != null) + { + GetOrCreate(prevNode, this.AddonTree).Print(null); + prevNode = prevNode->PrevSiblingNode; + } + var count = this.UldManager->NodeListCount; PrintNodeListAsTree(this.UldManager->NodeList, count, $"Node List [{count}]:", this.AddonTree, new(0f, 0.5f, 0.8f, 1f)); } @@ -92,11 +98,11 @@ internal unsafe class ComponentNodeTree : ResNodeTree ImGui.TextUnformatted( $"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}"); ImGui.TextUnformatted( - $"Text3: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText03.StringPtr))}"); + $"AvailableLines: {Marshal.PtrToStringAnsi(new(textInputComponent->AvailableLines.StringPtr))}"); ImGui.TextUnformatted( - $"Text4: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText04.StringPtr))}"); + $"HighlightedAutoTranslateOptionColorPrefix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.StringPtr))}"); ImGui.TextUnformatted( - $"Text5: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText05.StringPtr))}"); + $"HighlightedAutoTranslateOptionColorSuffix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.StringPtr))}"); break; case List: case TreeList: diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs index cfd06d726..3626b0574 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs @@ -84,7 +84,7 @@ internal unsafe partial class ResNodeTree : IDisposable /// An existing or newly-created instance of . internal static ResNodeTree GetOrCreate(AtkResNode* node, AddonTree addonTree) => addonTree.NodeTrees.TryGetValue((nint)node, out var nodeTree) ? nodeTree - : (int)node->Type > 1000 + : (int)node->Type >= 1000 ? new ComponentNodeTree(node, addonTree) : node->Type switch { diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs index 1a67ae9f5..060f28d1a 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs @@ -51,9 +51,10 @@ public readonly unsafe partial struct TimelineTree return; } - var count = this.Resource->AnimationCount; + var animationCount = this.Resource->AnimationCount; + var labelSetCount = this.Resource->LabelSetCount; - if (count > 0) + if (animationCount > 0) { using var tree = ImRaii.TreeNode($"Timeline##{(nint)this.node:X}timeline", SpanFullWidth); @@ -65,22 +66,35 @@ public readonly unsafe partial struct TimelineTree ShowStruct(this.NodeTimeline); - PrintFieldValuePairs( - ("Id", $"{this.NodeTimeline->Resource->Id}"), - ("Parent Time", $"{this.NodeTimeline->ParentFrameTime:F2} ({this.NodeTimeline->ParentFrameTime * 30:F0})"), - ("Frame Time", $"{this.NodeTimeline->FrameTime:F2} ({this.NodeTimeline->FrameTime * 30:F0})")); - - PrintFieldValuePairs(("Active Label Id", $"{this.NodeTimeline->ActiveLabelId}"), ("Duration", $"{this.NodeTimeline->LabelFrameIdxDuration}"), ("End Frame", $"{this.NodeTimeline->LabelEndFrameIdx}")); - ImGui.TextColored(new(0.6f, 0.6f, 0.6f, 1), "Animation List"); - - for (var a = 0; a < count; a++) + if (this.Resource->Animations is not null) { - var animation = this.Resource->Animations[a]; - var isActive = this.ActiveAnimation != null && &animation == this.ActiveAnimation; - this.PrintAnimation(animation, a, isActive, (nint)(this.NodeTimeline->Resource->Animations + (a * sizeof(AtkTimelineAnimation)))); + PrintFieldValuePairs( + ("Id", $"{this.NodeTimeline->Resource->Id}"), + ("Parent Time", $"{this.NodeTimeline->ParentFrameTime:F2} ({this.NodeTimeline->ParentFrameTime * 30:F0})"), + ("Frame Time", $"{this.NodeTimeline->FrameTime:F2} ({this.NodeTimeline->FrameTime * 30:F0})")); + + PrintFieldValuePairs(("Active Label Id", $"{this.NodeTimeline->ActiveLabelId}"), ("Duration", $"{this.NodeTimeline->LabelFrameIdxDuration}"), ("End Frame", $"{this.NodeTimeline->LabelEndFrameIdx}")); + ImGui.TextColored(new(0.6f, 0.6f, 0.6f, 1), "Animation List"); + + for (var a = 0; a < animationCount; a++) + { + var animation = this.Resource->Animations[a]; + var isActive = this.ActiveAnimation != null && &animation == this.ActiveAnimation; + this.PrintAnimation(animation, a, isActive, (nint)(this.NodeTimeline->Resource->Animations + a)); + } } } } + + if (labelSetCount > 0 && this.Resource->LabelSets is not null) + { + using var tree = ImRaii.TreeNode($"Timeline Label Sets##{(nint)this.node:X}LabelSets", SpanFullWidth); + + if (tree.Success) + { + this.DrawLabelSets(); + } + } } private static void GetFrameColumn(Span keyGroups, List columns, ushort endFrame) @@ -380,4 +394,63 @@ public readonly unsafe partial struct TimelineTree return columns; } + + private void DrawLabelSets() + { + PrintFieldValuePair("LabelSet", $"{(nint)this.NodeTimeline->Resource->LabelSets:X}"); + + ImGui.SameLine(); + + ShowStruct(this.NodeTimeline->Resource->LabelSets); + + PrintFieldValuePairs( + ("StartFrameIdx", $"{this.NodeTimeline->Resource->LabelSets->StartFrameIdx}"), + ("EndFrameIdx", $"{this.NodeTimeline->Resource->LabelSets->EndFrameIdx}")); + + using var labelSetTable = ImRaii.TreeNode("Entries"); + if (labelSetTable.Success) + { + var keyFrameGroup = this.Resource->LabelSets->LabelKeyGroup; + + using var table = ImRaii.Table($"##{(nint)this.node}labelSetKeyFrameTable", 7, Borders | SizingFixedFit | RowBg | NoHostExtendX); + if (table.Success) + { + ImGui.TableSetupColumn("Frame ID", WidthFixed); + ImGui.TableSetupColumn("Speed Start", WidthFixed); + ImGui.TableSetupColumn("Speed End", WidthFixed); + ImGui.TableSetupColumn("Interpolation", WidthFixed); + ImGui.TableSetupColumn("Label ID", WidthFixed); + ImGui.TableSetupColumn("Jump Behavior", WidthFixed); + ImGui.TableSetupColumn("Target Label ID", WidthFixed); + + ImGui.TableHeadersRow(); + + for (var l = 0; l < keyFrameGroup.KeyFrameCount; l++) + { + var keyFrame = keyFrameGroup.KeyFrames[l]; + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{keyFrame.FrameIdx}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{keyFrame.SpeedCoefficient1:F2}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{keyFrame.SpeedCoefficient2:F2}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{keyFrame.Interpolation}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{keyFrame.Value.Label.LabelId}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{keyFrame.Value.Label.JumpBehavior}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{keyFrame.Value.Label.JumpLabelId}"); + } + } + } + } } diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index d28086cbd..3b2109d70 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -121,6 +121,19 @@ internal class ConsoleWindow : Window, IDisposable /// Gets the queue where log entries that are not processed yet are stored. public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new(); + /// + /// Gets or sets the current text filter. + /// + public string TextFilter + { + get => this.textFilter; + set + { + this.textFilter = value; + this.RecompileLogFilter(); + } + } + /// public override void OnOpen() { @@ -620,24 +633,29 @@ internal class ConsoleWindow : Window, IDisposable ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll) || ImGui.IsItemDeactivatedAfterEdit()) { - this.compiledLogFilter = null; - this.exceptionLogFilter = null; - try - { - this.compiledLogFilter = new(this.textFilter, RegexOptions.IgnoreCase); - - this.QueueRefilter(); - } - catch (Exception e) - { - this.exceptionLogFilter = e; - } - - foreach (var log in this.logText) - log.HighlightMatches = null; + this.RecompileLogFilter(); } } + private void RecompileLogFilter() + { + this.compiledLogFilter = null; + this.exceptionLogFilter = null; + try + { + this.compiledLogFilter = new(this.textFilter, RegexOptions.IgnoreCase); + + this.QueueRefilter(); + } + catch (Exception e) + { + this.exceptionLogFilter = e; + } + + foreach (var log in this.logText) + log.HighlightMatches = null; + } + private void DrawSettingsPopup() { if (ImGui.Checkbox("Open at startup", ref this.autoOpen)) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs index aa4718143..5f1792fcc 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs @@ -1,4 +1,5 @@ -using System.Numerics; +using System.Linq; +using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Game.Gui.FlyText; @@ -38,13 +39,15 @@ internal class FlyTextWidget : IDataWindowWidget /// public void Draw() { - if (ImGui.BeginCombo("Kind", this.flyKind.ToString())) + if (ImGui.BeginCombo("Kind", $"{this.flyKind} ({(int)this.flyKind})")) { - var names = Enum.GetNames(typeof(FlyTextKind)); - for (var i = 0; i < names.Length; i++) + var values = Enum.GetValues().Distinct(); + foreach (var value in values) { - if (ImGui.Selectable($"{names[i]} ({i})")) - this.flyKind = (FlyTextKind)i; + if (ImGui.Selectable($"{value} ({(int)value})")) + { + this.flyKind = value; + } } ImGui.EndCombo(); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index 96ff5a812..c6e491ec0 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -1,7 +1,13 @@ -using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; using Dalamud.Bindings.ImGui; +using Dalamud.Game; +using Dalamud.Game.Addon.Lifecycle; using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Component.GUI; using Serilog; using Windows.Win32.Foundation; using Windows.Win32.UI.WindowsAndMessaging; @@ -11,17 +17,40 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying hook information. /// -internal class HookWidget : IDataWindowWidget +internal unsafe class HookWidget : IDataWindowWidget { + private readonly List hookStressTestList = []; + private Hook? messageBoxMinHook; private bool hookUseMinHook; + private int hookStressTestCount = 0; + private int hookStressTestMax = 1000; + private int hookStressTestWait = 100; + private int hookStressTestMaxDegreeOfParallelism = 10; + private StressTestHookTarget hookStressTestHookTarget = StressTestHookTarget.Random; + private bool hookStressTestRunning = false; + + private MessageBoxWDelegate? messageBoxWOriginal; + private AddonFinalizeDelegate? addonFinalizeOriginal; + + private AddonLifecycleAddressResolver? address; + private delegate int MessageBoxWDelegate( IntPtr hWnd, [MarshalAs(UnmanagedType.LPWStr)] string text, [MarshalAs(UnmanagedType.LPWStr)] string caption, MESSAGEBOX_STYLE type); + private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); + + private enum StressTestHookTarget + { + MessageBoxW, + AddonFinalize, + Random, + } + /// public string DisplayName { get; init; } = "Hook"; @@ -35,6 +64,9 @@ internal class HookWidget : IDataWindowWidget public void Load() { this.Ready = true; + + this.address = new AddonLifecycleAddressResolver(); + this.address.Setup(Service.Get()); } /// @@ -42,7 +74,9 @@ internal class HookWidget : IDataWindowWidget { try { - ImGui.Checkbox("Use MinHook", ref this.hookUseMinHook); + ImGui.Checkbox("Use MinHook (only for regular hooks, AsmHook is Reloaded-only)", ref this.hookUseMinHook); + + ImGui.Separator(); if (ImGui.Button("Create")) this.messageBoxMinHook = Hook.FromSymbol("User32", "MessageBoxW", this.MessageBoxWDetour, this.hookUseMinHook); @@ -67,18 +101,94 @@ internal class HookWidget : IDataWindowWidget if (this.messageBoxMinHook != null) ImGui.Text("Enabled: " + this.messageBoxMinHook?.IsEnabled); + + ImGui.Separator(); + + ImGui.BeginDisabled(this.hookStressTestRunning); + ImGui.Text("Stress Test"); + + if (ImGui.InputInt("Max", ref this.hookStressTestMax)) + this.hookStressTestCount = 0; + + ImGui.InputInt("Wait (ms)", ref this.hookStressTestWait); + ImGui.InputInt("Max Degree of Parallelism", ref this.hookStressTestMaxDegreeOfParallelism); + + if (ImGui.BeginCombo("Target", HookTargetToString(this.hookStressTestHookTarget))) + { + foreach (var target in Enum.GetValues()) + { + if (ImGui.Selectable(HookTargetToString(target), this.hookStressTestHookTarget == target)) + this.hookStressTestHookTarget = target; + } + + ImGui.EndCombo(); + } + + if (ImGui.Button("Stress Test")) + { + Task.Run(() => + { + this.hookStressTestRunning = true; + this.hookStressTestCount = 0; + Parallel.For( + 0, + this.hookStressTestMax, + new ParallelOptions + { + MaxDegreeOfParallelism = this.hookStressTestMaxDegreeOfParallelism, + }, + _ => + { + this.hookStressTestList.Add(this.HookTarget(this.hookStressTestHookTarget)); + this.hookStressTestCount++; + Thread.Sleep(this.hookStressTestWait); + }); + }).ContinueWith(t => + { + if (t.IsFaulted) + { + Log.Error(t.Exception, "Stress test failed"); + } + else + { + Log.Information("Stress test completed"); + } + + this.hookStressTestRunning = false; + this.hookStressTestList.ForEach(hook => + { + hook.Dispose(); + }); + this.hookStressTestList.Clear(); + }); + } + + ImGui.EndDisabled(); + + ImGui.TextUnformatted("Status: " + (this.hookStressTestRunning ? "Running" : "Idle")); + ImGui.ProgressBar(this.hookStressTestCount / (float)this.hookStressTestMax, new System.Numerics.Vector2(0, 0), $"{this.hookStressTestCount}/{this.hookStressTestMax}"); } catch (Exception ex) { - Log.Error(ex, "MinHook error caught"); + Log.Error(ex, "Hook error caught"); } } + private static string HookTargetToString(StressTestHookTarget target) + { + return target switch + { + StressTestHookTarget.MessageBoxW => "MessageBoxW (Hook)", + StressTestHookTarget.AddonFinalize => "AddonFinalize (Hook)", + _ => target.ToString(), + }; + } + private int MessageBoxWDetour(IntPtr hwnd, string text, string caption, MESSAGEBOX_STYLE type) { Log.Information("[DATAHOOK] {Hwnd} {Text} {Caption} {Type}", hwnd, text, caption, type); - var result = this.messageBoxMinHook!.Original(hwnd, "Cause Access Violation?", caption, MESSAGEBOX_STYLE.MB_YESNO); + var result = this.messageBoxWOriginal!(hwnd, "Cause Access Violation?", caption, MESSAGEBOX_STYLE.MB_YESNO); if (result == (int)MESSAGEBOX_RESULT.IDYES) { @@ -87,4 +197,52 @@ internal class HookWidget : IDataWindowWidget return result; } + + private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) + { + Log.Information("OnAddonFinalize"); + this.addonFinalizeOriginal!(unitManager, atkUnitBase); + } + + private void OnAddonUpdate(AtkUnitBase* thisPtr, float delta) + { + Log.Information("OnAddonUpdate"); + } + + private IDalamudHook HookMessageBoxW() + { + var hook = Hook.FromSymbol( + "User32", + "MessageBoxW", + this.MessageBoxWDetour, + this.hookUseMinHook); + + this.messageBoxWOriginal = hook.Original; + hook.Enable(); + return hook; + } + + private IDalamudHook HookAddonFinalize() + { + var hook = Hook.FromAddress(this.address!.AddonFinalize, this.OnAddonFinalize); + + this.addonFinalizeOriginal = hook.Original; + hook.Enable(); + return hook; + } + + private IDalamudHook HookTarget(StressTestHookTarget target) + { + if (target == StressTestHookTarget.Random) + { + target = (StressTestHookTarget)Random.Shared.Next(0, 2); + } + + return target switch + { + StressTestHookTarget.MessageBoxW => this.HookMessageBoxW(), + StressTestHookTarget.AddonFinalize => this.HookAddonFinalize(), + _ => throw new ArgumentOutOfRangeException(nameof(target), target, null), + }; + } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs index 25c22e04b..af4d0e6e1 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs @@ -23,7 +23,7 @@ internal class InventoryWidget : IDataWindowWidget { private DataManager dataManager; private TextureManager textureManager; - private InventoryType? selectedInventoryType = InventoryType.Inventory1; + private GameInventoryType? selectedInventoryType = GameInventoryType.Inventory1; /// public string[]? CommandShortcuts { get; init; } = ["inv", "inventory"]; @@ -53,7 +53,7 @@ internal class InventoryWidget : IDataWindowWidget ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); - this.DrawInventoryType((InventoryType)this.selectedInventoryType); + this.DrawInventoryType(this.selectedInventoryType.Value); } private static string StripSoftHypen(string input) @@ -71,9 +71,9 @@ internal class InventoryWidget : IDataWindowWidget ImGui.TableSetupScrollFreeze(2, 1); ImGui.TableHeadersRow(); - foreach (var inventoryType in Enum.GetValues()) + foreach (var inventoryType in Enum.GetValues()) { - var items = GameInventoryItem.GetReadOnlySpanOfInventory((GameInventoryType)inventoryType); + var items = GameInventoryItem.GetReadOnlySpanOfInventory(inventoryType); using var itemDisabled = ImRaii.Disabled(items.IsEmpty); @@ -95,7 +95,7 @@ internal class InventoryWidget : IDataWindowWidget if (ImGui.MenuItem("Copy Address")) { - var container = InventoryManager.Instance()->GetInventoryContainer(inventoryType); + var container = InventoryManager.Instance()->GetInventoryContainer((InventoryType)inventoryType); ImGui.SetClipboardText($"0x{(nint)container:X}"); } } @@ -106,9 +106,9 @@ internal class InventoryWidget : IDataWindowWidget } } - private unsafe void DrawInventoryType(InventoryType inventoryType) + private unsafe void DrawInventoryType(GameInventoryType inventoryType) { - var items = GameInventoryItem.GetReadOnlySpanOfInventory((GameInventoryType)inventoryType); + var items = GameInventoryItem.GetReadOnlySpanOfInventory(inventoryType); if (items.IsEmpty) { ImGui.TextUnformatted($"{inventoryType} is empty."); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs index f13a590b3..5f0eb9444 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs @@ -20,7 +20,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; internal class NounProcessorWidget : IDataWindowWidget { /// A list of German grammatical cases. - internal static readonly string[] GermanCases = ["Nominative", "Genitive", "Dative", "Accusative"]; + internal static readonly string[] GermanCases = [string.Empty, "Nominative", "Genitive", "Dative", "Accusative"]; private static readonly Type[] NounSheets = [ typeof(Aetheryte), @@ -155,7 +155,7 @@ internal class NounProcessorWidget : IDataWindowWidget GrammaticalCase = grammaticalCase, }; var output = nounProcessor.ProcessNoun(nounParams).ExtractText().Replace("\"", "\\\""); - var caseParam = language == ClientLanguage.German ? $"(int)GermanCases.{GermanCases[grammaticalCase]}" : "1"; + var caseParam = language == ClientLanguage.German ? $"(int)GermanCases.{GermanCases[grammaticalCase + 1]}" : "1"; sb.AppendLine($"new(nameof(LSheets.{sheetType.Name}), {this.rowId}, ClientLanguage.{language}, {this.amount}, (int){articleTypeEnumType.Name}.{Enum.GetName(articleTypeEnumType, articleType)}, {caseParam}, \"{output}\"),"); } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index a49c01e92..11c10e515 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -7,6 +7,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; +using Dalamud.Game.ClientState; using Dalamud.Game.Text.Evaluator; using Dalamud.Game.Text.Noun.Enums; using Dalamud.Game.Text.SeStringHandling; @@ -90,6 +91,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget { MacroCode.LowerHead, ["String"] }, { MacroCode.ColorType, ["ColorType"] }, { MacroCode.EdgeColorType, ["ColorType"] }, + { MacroCode.Ruby, ["StandardText", "RubyText"] }, { MacroCode.Digit, ["Value", "TargetLength"] }, { MacroCode.Ordinal, ["Value"] }, { MacroCode.Sound, ["IsJingle", "SoundId"] }, @@ -132,8 +134,8 @@ internal class SeStringCreatorWidget : IDataWindowWidget new TextEntry(TextEntryType.Macro, ""), new TextEntry(TextEntryType.Macro, ""), new TextEntry(TextEntryType.String, "Dalamud"), - new TextEntry(TextEntryType.Macro, ""), - new TextEntry(TextEntryType.Macro, ""), + new TextEntry(TextEntryType.Macro, ""), + new TextEntry(TextEntryType.Macro, ""), new TextEntry(TextEntryType.Macro, " "), ]; @@ -165,7 +167,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget /// public void Load() { - this.language = Service.Get().EffectiveLanguage.ToClientLanguage(); + this.language = Service.Get().ClientLanguage; this.UpdateInputString(false); this.Ready = true; } @@ -473,7 +475,12 @@ internal class SeStringCreatorWidget : IDataWindowWidget } } - RaptureLogModule.Instance()->PrintString(Service.Get().Evaluate(sb.ToReadOnlySeString())); + var evaluated = Service.Get().Evaluate( + sb.ToReadOnlySeString(), + this.localParameters, + this.language); + + RaptureLogModule.Instance()->PrintString(evaluated); } if (this.entries.Count != 0) @@ -1011,7 +1018,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget ImGui.TextUnformatted(Enum.GetName(articleTypeEnumType, u32)); } - if (macroCode is MacroCode.DeNoun && exprIdx == 4 && u32 is >= 0 and <= 3) + if (macroCode is MacroCode.DeNoun && exprIdx == 4 && u32 is >= 0 and <= 4) { ImGui.SameLine(); ImGui.TextUnformatted(NounProcessorWidget.GermanCases[u32]); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs index 8971f7fc8..16582793a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs @@ -237,29 +237,42 @@ internal class ServicesWidget : IDataWindowWidget } } - if (ImGui.CollapsingHeader("Plugin-facing Services")) + if (ImGui.CollapsingHeader("Singleton Services")) { foreach (var instance in container.Instances) { - var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key); var isPublic = instance.Key.IsPublic; ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})"); - using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface)) - { - ImGui.Text( - hasInterface - ? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}" - : "\t => NO INTERFACE!!!"); - } - if (isPublic) { using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); ImGui.Text("\t => PUBLIC!!!"); } + switch (instance.Value.Visibility) + { + case ObjectInstanceVisibility.Internal: + ImGui.Text("\t => Internally resolved"); + break; + + case ObjectInstanceVisibility.ExposedToPlugins: + var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface)) + { + ImGui.Text("\t => Exposed to plugins!"); + ImGui.Text( + hasInterface + ? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}" + : "\t => NO INTERFACE!!!"); + } + + break; + default: + throw new ArgumentOutOfRangeException(); + } + ImGuiHelpers.ScaledDummy(2); } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index ba707e0ca..9277d01f3 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -180,6 +180,15 @@ internal class TexWidget : IDataWindowWidget ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); + if (!this.textureManager.HasClipboardImage()) + { + ImGuiComponents.DisabledButton("Paste from Clipboard"); + } + else if (ImGui.Button("Paste from Clipboard")) + { + this.addedTextures.Add(new(Api10: this.textureManager.CreateFromClipboardAsync())); + } + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon))) { ImGui.PushID(nameof(this.DrawGetFromGameIcon)); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 0ae72fc29..993cfecaa 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -226,6 +226,7 @@ internal class PluginInstallerWindow : Window, IDisposable IsInstallableOutdated = 1 << 5, IsOrphan = 1 << 6, IsTesting = 1 << 7, + IsIncompatible = 1 << 8, } private enum InstalledPluginListFilter @@ -281,11 +282,15 @@ internal class PluginInstallerWindow : Window, IDisposable var pluginManager = Service.Get(); _ = pluginManager.ReloadPluginMastersAsync(); - Service.Get().ScanDevPlugins(); + _ = pluginManager.ScanDevPluginsAsync(); + + if (!this.isSearchTextPrefilled) + { + this.searchText = string.Empty; + this.sortKind = PluginSortKind.Alphabetical; + this.filterText = Locs.SortBy_Alphabetical; + } - if (!this.isSearchTextPrefilled) this.searchText = string.Empty; - this.sortKind = PluginSortKind.Alphabetical; - this.filterText = Locs.SortBy_Alphabetical; this.adaptiveSort = true; if (this.updateStatus == OperationStatus.Complete || this.updateStatus == OperationStatus.Idle) @@ -361,11 +366,20 @@ internal class PluginInstallerWindow : Window, IDisposable { this.isSearchTextPrefilled = false; this.searchText = string.Empty; + if (this.sortKind == PluginSortKind.SearchScore) + { + this.sortKind = PluginSortKind.Alphabetical; + this.filterText = Locs.SortBy_Alphabetical; + this.ResortPlugins(); + } } else { this.isSearchTextPrefilled = true; this.searchText = text; + this.sortKind = PluginSortKind.SearchScore; + this.filterText = Locs.SortBy_SearchScore; + this.ResortPlugins(); } } @@ -458,6 +472,36 @@ internal class PluginInstallerWindow : Window, IDisposable configuration.QueueSave(); } + private static void DrawProgressBar(IEnumerable items, Func pendingFunc, Func totalFunc, Action renderPending) + { + var windowSize = ImGui.GetWindowSize(); + + var numLoaded = 0; + var total = 0; + + var itemsArray = items as T[] ?? items.ToArray(); + var allPending = itemsArray.Where(pendingFunc) + .ToArray(); + var allLoadedOrLoading = itemsArray.Count(totalFunc); + + // Cap number of items we show to avoid clutter + const int maxShown = 3; + foreach (var repo in allPending.Take(maxShown)) + { + renderPending(repo); + } + + ImGuiHelpers.ScaledDummy(10); + + numLoaded += allLoadedOrLoading - allPending.Length; + total += allLoadedOrLoading; + if (numLoaded != total) + { + ImGui.SetCursorPosX(windowSize.X / 3); + ImGui.ProgressBar(numLoaded / (float)total, new Vector2(windowSize.X / 3, 50), $"{numLoaded}/{total}"); + } + } + private void SetOpenPage(PluginInstallerOpenKind kind) { switch (kind) @@ -486,6 +530,12 @@ internal class PluginInstallerWindow : Window, IDisposable // Plugins category this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All; break; + case PluginInstallerOpenKind.DalamudChangelogs: + // Changelog group + this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Changelog; + // Dalamud category + this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.DalamudChangelogs; + break; default: throw new ArgumentOutOfRangeException(nameof(kind), kind, null); } @@ -523,7 +573,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.GetWindowDrawList().AddRectFilled( ImGui.GetWindowPos() + new Vector2(0, titleHeight), ImGui.GetWindowPos() + windowSize, - 0xCC000000, + ImGui.ColorConvertFloat4ToU32(new(0f, 0f, 0f, 0.8f * ImGui.GetStyle().Alpha)), ImGui.GetStyle().WindowRounding, ImDrawFlags.RoundCornersBottom); ImGui.PopClipRect(); @@ -555,40 +605,29 @@ internal class PluginInstallerWindow : Window, IDisposable if (pluginManager.PluginsReady && !pluginManager.ReposReady) { ImGuiHelpers.CenteredText("Loading repositories..."); + ImGuiHelpers.ScaledDummy(10); + + DrawProgressBar(pluginManager.Repos, x => x.State != PluginRepositoryState.Success && + x.State != PluginRepositoryState.Fail && + x.IsEnabled, + x => x.IsEnabled, + x => ImGuiHelpers.CenteredText($"Loading {x.PluginMasterUrl}")); } else if (!pluginManager.PluginsReady && pluginManager.ReposReady) { ImGuiHelpers.CenteredText("Loading installed plugins..."); + ImGuiHelpers.ScaledDummy(10); + + DrawProgressBar(pluginManager.InstalledPlugins, x => x.State == PluginState.Loading, + x => x.State is PluginState.Loaded or + PluginState.LoadError or + PluginState.Loading, + x => ImGuiHelpers.CenteredText($"Loading {x.Name}")); } else { ImGuiHelpers.CenteredText("Loading repositories and plugins..."); } - - var currentProgress = 0; - var total = 0; - - var pendingRepos = pluginManager.Repos.ToArray() - .Where(x => (x.State != PluginRepositoryState.Success && - x.State != PluginRepositoryState.Fail) && - x.IsEnabled) - .ToArray(); - var allRepoCount = - pluginManager.Repos.Count(x => x.State != PluginRepositoryState.Fail && x.IsEnabled); - - foreach (var repo in pendingRepos) - { - ImGuiHelpers.CenteredText($"{repo.PluginMasterUrl}: {repo.State}"); - } - - currentProgress += allRepoCount - pendingRepos.Length; - total += allRepoCount; - - if (currentProgress != total) - { - ImGui.SetCursorPosX(windowSize.X / 3); - ImGui.ProgressBar(currentProgress / (float)total, new Vector2(windowSize.X / 3, 50)); - } } break; @@ -599,11 +638,27 @@ internal class PluginInstallerWindow : Window, IDisposable throw new ArgumentOutOfRangeException(); } - if (DateTime.Now - this.timeLoaded > TimeSpan.FromSeconds(90) && !pluginManager.PluginsReady) + if (DateTime.Now - this.timeLoaded > TimeSpan.FromSeconds(30) && !pluginManager.PluginsReady) { + ImGuiHelpers.ScaledDummy(10); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); ImGuiHelpers.CenteredText("One of your plugins may be blocking the installer."); + ImGuiHelpers.CenteredText("You can try restarting in safe mode, and deleting the plugin."); ImGui.PopStyleColor(); + + ImGuiHelpers.BeginHorizontalButtonGroup() + .Add( + "Restart in Safe Mode", + () => + { + var config = Service.Get(); + config.PluginSafeMode = true; + config.ForceSave(); + Dalamud.RestartGame(); + }) + .SetCentered(true) + .WithHeight(30) + .Draw(); } } } @@ -761,7 +816,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.SameLine(); if (ImGui.Button(Locs.FooterButton_ScanDevPlugins)) { - pluginManager.ScanDevPlugins(); + _ = pluginManager.ScanDevPluginsAsync(); } } @@ -2137,7 +2192,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.PushStyleVar(ImGuiStyleVar.Alpha, overlayAlpha); if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable)) ImGui.Image(this.imageCache.UpdateIcon.Handle, iconSize); - else if ((flags.HasFlag(PluginHeaderFlags.HasTrouble) && !pluginDisabled) || flags.HasFlag(PluginHeaderFlags.IsOrphan)) + else if ((flags.HasFlag(PluginHeaderFlags.HasTrouble) && !pluginDisabled) || flags.HasFlag(PluginHeaderFlags.IsOrphan) || flags.HasFlag(PluginHeaderFlags.IsIncompatible)) ImGui.Image(this.imageCache.TroubleIcon.Handle, iconSize); else if (flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated)) ImGui.Image(this.imageCache.OutdatedInstallableIcon.Handle, iconSize); @@ -2213,9 +2268,14 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.SetCursorPos(cursor); // Outdated warning - if (plugin is { IsOutdated: true, IsBanned: false } || flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated)) + if (flags.HasFlag(PluginHeaderFlags.IsIncompatible)) { - ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImGui.TextWrapped(Locs.PluginBody_Incompatible); + } + else if (plugin is { IsOutdated: true, IsBanned: false } || flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated)) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); var bodyText = Locs.PluginBody_Outdated + " "; if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable)) @@ -2224,7 +2284,6 @@ internal class PluginInstallerWindow : Window, IDisposable bodyText += Locs.PluginBody_Outdated_WaitForUpdate; ImGui.TextWrapped(bodyText); - ImGui.PopStyleColor(); } else if (plugin is { IsBanned: true }) { @@ -2393,6 +2452,14 @@ internal class PluginInstallerWindow : Window, IDisposable var effectiveApiLevel = useTesting && manifest.TestingDalamudApiLevel != null ? manifest.TestingDalamudApiLevel.Value : manifest.DalamudApiLevel; var isOutdated = effectiveApiLevel < PluginManager.DalamudApiLevel; + var isIncompatible = manifest.MinimumDalamudVersion != null && + manifest.MinimumDalamudVersion > Util.AssemblyVersionParsed; + + var enableInstallButton = this.updateStatus != OperationStatus.InProgress && + this.installStatus != OperationStatus.InProgress && + !isOutdated && + !isIncompatible; + // Check for valid versions if ((useTesting && manifest.TestingAssemblyVersion == null) || manifest.AssemblyVersion == null) { @@ -2417,6 +2484,11 @@ internal class PluginInstallerWindow : Window, IDisposable label += Locs.PluginTitleMod_TestingAvailable; } + if (isIncompatible) + { + label += Locs.PluginTitleMod_Incompatible; + } + var isThirdParty = manifest.SourceRepo.IsThirdParty; ImGui.PushID($"available{index}{manifest.InternalName}"); @@ -2430,6 +2502,8 @@ internal class PluginInstallerWindow : Window, IDisposable flags |= PluginHeaderFlags.IsInstallableOutdated; if (useTesting || manifest.IsTestingExclusive) flags |= PluginHeaderFlags.IsTesting; + if (isIncompatible) + flags |= PluginHeaderFlags.IsIncompatible; if (this.DrawPluginCollapsingHeader(label, null, manifest, flags, () => this.DrawAvailablePluginContextMenu(manifest), index)) { @@ -2457,9 +2531,6 @@ internal class PluginInstallerWindow : Window, IDisposable ImGuiHelpers.ScaledDummy(5); - // Controls - var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress || isOutdated; - var versionString = useTesting ? $"{manifest.TestingAssemblyVersion}" : $"{manifest.AssemblyVersion}"; @@ -2468,7 +2539,7 @@ internal class PluginInstallerWindow : Window, IDisposable { ImGuiComponents.DisabledButton(Locs.PluginButton_SafeMode); } - else if (disabled) + else if (!enableInstallButton) { ImGuiComponents.DisabledButton(Locs.PluginButton_InstallVersion(versionString)); } @@ -2713,7 +2784,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}"); - var applicableChangelog = plugin.IsTesting ? remoteManifest?.Changelog : remoteManifest?.TestingChangelog; + var applicableChangelog = plugin.IsTesting ? remoteManifest?.TestingChangelog : remoteManifest?.Changelog; var hasChangelog = !applicableChangelog.IsNullOrWhitespace(); var didDrawApplicableChangelogInsideCollapsible = false; @@ -3096,11 +3167,13 @@ internal class PluginInstallerWindow : Window, IDisposable { ImGuiComponents.DisabledToggleButton(toggleId, this.loadingIndicatorKind == LoadingIndicatorKind.EnablingSingle); } - else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled) + else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled || pluginManager.SafeMode) { ImGuiComponents.DisabledToggleButton(toggleId, isLoadedAndUnloadable); - if (inMultipleProfiles && ImGui.IsItemHovered()) + if (pluginManager.SafeMode && ImGui.IsItemHovered()) + ImGui.SetTooltip(Locs.PluginButtonToolTip_SafeMode); + else if (inMultipleProfiles && ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.PluginButtonToolTip_NeedsToBeInSingleProfile); else if (inSingleNonDefaultProfileWhichIsDisabled && ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.PluginButtonToolTip_SingleProfileDisabled(profilesThatWantThisPlugin.First().Name)); @@ -3494,6 +3567,24 @@ internal class PluginInstallerWindow : Window, IDisposable { ImGui.SetTooltip(Locs.PluginButtonToolTip_AutomaticReloading); } + + // Error Notifications + ImGui.PushStyleColor(ImGuiCol.Button, plugin.NotifyForErrors ? greenColor : redColor); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.NotifyForErrors ? greenColor : redColor); + + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Bolt)) + { + plugin.NotifyForErrors ^= true; + configuration.QueueSave(); + } + + ImGui.PopStyleColor(2); + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(Locs.PluginButtonToolTip_NotifyForErrors); + } } } @@ -4047,6 +4138,8 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginTitleMod_TestingAvailable => Loc.Localize("InstallerTestingAvailable", " (has testing version)"); + public static string PluginTitleMod_Incompatible => Loc.Localize("InstallerTitleModIncompatible", " (incompatible)"); + public static string PluginTitleMod_DevPlugin => Loc.Localize("InstallerDevPlugin", " (dev plugin)"); public static string PluginTitleMod_UpdateFailed => Loc.Localize("InstallerUpdateFailed", " (update failed)"); @@ -4103,6 +4196,8 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible."); + public static string PluginBody_Incompatible => Loc.Localize("InstallerIncompatiblePluginBody ", "This plugin is incompatible with your version of Dalamud. Please attempt to restart your game."); + public static string PluginBody_Outdated_WaitForUpdate => Loc.Localize("InstallerOutdatedWaitForUpdate", "Please wait for it to be updated by its author."); public static string PluginBody_Outdated_CanNowUpdate => Loc.Localize("InstallerOutdatedCanNowUpdate", "An update is available for installation."); @@ -4160,6 +4255,8 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginButtonToolTip_AutomaticReloading => Loc.Localize("InstallerAutomaticReloading", "Automatic reloading"); + public static string PluginButtonToolTip_NotifyForErrors => Loc.Localize("InstallerNotifyForErrors", "Show Dalamud notifications when this plugin is creating errors"); + public static string PluginButtonToolTip_DeletePlugin => Loc.Localize("InstallerDeletePlugin ", "Delete plugin"); public static string PluginButtonToolTip_DeletePluginRestricted => Loc.Localize("InstallerDeletePluginRestricted", "Cannot delete right now - please restart the game."); @@ -4180,6 +4277,8 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginButtonToolTip_NeedsToBeInSingleProfile => Loc.Localize("InstallerUnloadNeedsToBeInSingleProfile", "This plugin is in more than one collection. If you want to enable or disable it, please do so by enabling or disabling the collections it is in.\nIf you want to manage it here, make sure it is only in a single collection."); + public static string PluginButtonToolTip_SafeMode => Loc.Localize("InstallerButtonSafeModeTooltip", "Cannot enable plugins in safe mode."); + public static string PluginButtonToolTip_SingleProfileDisabled(string name) => Loc.Localize("InstallerSingleProfileDisabled", "The collection '{0}' which contains this plugin is disabled.\nPlease enable it in the collections manager to toggle the plugin individually.").Format(name); #endregion diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index ecbb8ff7d..999e25316 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -224,6 +224,9 @@ internal class ProfileManagerWidget ImGuiHelpers.ScaledDummy(3); ImGui.SameLine(); + // Center text in frame height + var textHeight = ImGui.CalcTextSize(profile.Name); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (ImGui.GetFrameHeight() / 2) - (textHeight.Y / 2)); ImGui.Text(profile.Name); ImGui.SameLine(); @@ -263,6 +266,17 @@ internal class ProfileManagerWidget didAny = true; ImGuiHelpers.ScaledDummy(2); + + // Separator if not the last item + if (profile != profman.Profiles.Last()) + { + // Very light grey + ImGui.PushStyleColor(ImGuiCol.Border, ImGuiColors.DalamudGrey.WithAlpha(0.2f)); + ImGui.Separator(); + ImGui.PopStyleColor(); + + ImGuiHelpers.ScaledDummy(2); + } } if (toCloneGuid != null) @@ -386,10 +400,19 @@ internal class ProfileManagerWidget ImGuiHelpers.ScaledDummy(5); - var enableAtBoot = profile.AlwaysEnableAtBoot; - if (ImGui.Checkbox(Locs.AlwaysEnableAtBoot, ref enableAtBoot)) + ImGui.TextUnformatted(Locs.StartupBehavior); + if (ImGui.BeginCombo("##startupBehaviorPicker", Locs.PolicyToLocalisedName(profile.StartupPolicy))) { - profile.AlwaysEnableAtBoot = enableAtBoot; + foreach (var policy in Enum.GetValues(typeof(ProfileModelV1.ProfileStartupPolicy)).Cast()) + { + var name = Locs.PolicyToLocalisedName(policy); + if (ImGui.Selectable(name, profile.StartupPolicy == policy)) + { + profile.StartupPolicy = policy; + } + } + + ImGui.EndCombo(); } ImGuiHelpers.ScaledDummy(5); @@ -514,6 +537,15 @@ internal class ProfileManagerWidget if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.RemovePlugin); + + // Separator if not the last item + if (profileEntry != profile.Plugins.Last()) + { + // Very light grey + ImGui.PushStyleColor(ImGuiCol.Border, ImGuiColors.DalamudGrey.WithAlpha(0.2f)); + ImGui.Separator(); + ImGui.PopStyleColor(); + } } if (wantRemovePluginGuid != null) @@ -554,6 +586,9 @@ internal class ProfileManagerWidget private static class Locs { + public static string StartupBehavior => + Loc.Localize("ProfileManagerStartupBehavior", "Startup behavior"); + public static string TooltipEnableDisable => Loc.Localize("ProfileManagerEnableDisableHint", "Enable/Disable this collection"); @@ -567,9 +602,6 @@ internal class ProfileManagerWidget public static string NoPluginsInProfile => Loc.Localize("ProfileManagerNoPluginsInProfile", "Collection has no plugins!"); - public static string AlwaysEnableAtBoot => - Loc.Localize("ProfileManagerAlwaysEnableAtBoot", "Always enable when game starts"); - public static string DeleteProfileHint => Loc.Localize("ProfileManagerDeleteProfile", "Delete this collection"); public static string CopyToClipboardHint => @@ -647,5 +679,22 @@ internal class ProfileManagerWidget public static string NotInstalled(string name) => Loc.Localize("ProfileManagerNotInstalled", "{0} (Not Installed)").Format(name); + + public static string PolicyToLocalisedName(ProfileModelV1.ProfileStartupPolicy policy) + { + return policy switch + { + ProfileModelV1.ProfileStartupPolicy.RememberState => Loc.Localize( + "ProfileManagerRememberState", + "Remember state"), + ProfileModelV1.ProfileStartupPolicy.AlwaysEnable => Loc.Localize( + "ProfileManagerAlwaysEnableAtBoot", + "Always enable at boot"), + ProfileModelV1.ProfileStartupPolicy.AlwaysDisable => Loc.Localize( + "ProfileManagerAlwaysDisableAtBoot", + "Always disable at boot"), + _ => throw new ArgumentOutOfRangeException(nameof(policy), policy, null), + }; + } } } diff --git a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs index 946bdc1a6..05de41203 100644 --- a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs @@ -9,6 +9,7 @@ using Dalamud.Hooking.Internal; using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; @@ -44,51 +45,54 @@ internal class PluginStatWindow : Window { var pluginManager = Service.Get(); - if (!ImGui.BeginTabBar("Stat Tabs")) + using var tabBar = ImRaii.TabBar("Stat Tabs"); + if (!tabBar) return; - if (ImGui.BeginTabItem("Draw times")) + using (var tabItem = ImRaii.TabItem("Draw times")) { - var doStats = UiBuilder.DoStats; - - if (ImGui.Checkbox("Enable Draw Time Tracking", ref doStats)) + if (tabItem) { - UiBuilder.DoStats = doStats; - } + var doStats = UiBuilder.DoStats; - if (doStats) - { - ImGui.SameLine(); - if (ImGui.Button("Reset")) + if (ImGui.Checkbox("Enable Draw Time Tracking", ref doStats)) { - foreach (var plugin in pluginManager.InstalledPlugins) - { - if (plugin.DalamudInterface != null) - { - plugin.DalamudInterface.LocalUiBuilder.LastDrawTime = -1; - plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime = -1; - plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Clear(); - } - } + UiBuilder.DoStats = doStats; } - var loadedPlugins = pluginManager.InstalledPlugins.Where(plugin => plugin.State == PluginState.Loaded); - var totalLast = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.LastDrawTime ?? 0); - var totalAverage = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0); + if (doStats) + { + ImGui.SameLine(); + if (ImGui.Button("Reset")) + { + foreach (var plugin in pluginManager.InstalledPlugins) + { + if (plugin.DalamudInterface != null) + { + plugin.DalamudInterface.LocalUiBuilder.LastDrawTime = -1; + plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime = -1; + plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Clear(); + } + } + } - ImGuiComponents.TextWithLabel("Total Last", $"{totalLast / 10000f:F4}ms", "All last draw times added together"); - ImGui.SameLine(); - ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage / 10000f:F4}ms", "All average draw times added together"); - ImGui.SameLine(); - ImGuiComponents.TextWithLabel("Collective Average", $"{(loadedPlugins.Any() ? totalAverage / loadedPlugins.Count() / 10000f : 0):F4}ms", "Average of all average draw times"); + var loadedPlugins = pluginManager.InstalledPlugins.Where(plugin => plugin.State == PluginState.Loaded); + var totalLast = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.LastDrawTime ?? 0); + var totalAverage = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0); - ImGui.InputTextWithHint( - "###PluginStatWindow_DrawSearch", - "Search", - ref this.drawSearchText, - 500); + ImGuiComponents.TextWithLabel("Total Last", $"{totalLast / 10000f:F4}ms", "All last draw times added together"); + ImGui.SameLine(); + ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage / 10000f:F4}ms", "All average draw times added together"); + ImGui.SameLine(); + ImGuiComponents.TextWithLabel("Collective Average", $"{(loadedPlugins.Any() ? totalAverage / loadedPlugins.Count() / 10000f : 0):F4}ms", "Average of all average draw times"); - if (ImGui.BeginTable( + ImGui.InputTextWithHint( + "###PluginStatWindow_DrawSearch", + "Search", + ref this.drawSearchText, + 500); + + using var table = ImRaii.Table( "##PluginStatsDrawTimes", 4, ImGuiTableFlags.RowBg @@ -97,99 +101,100 @@ internal class PluginStatWindow : Window | ImGuiTableFlags.Resizable | ImGuiTableFlags.ScrollY | ImGuiTableFlags.Reorderable - | ImGuiTableFlags.Hideable)) - { - ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableSetupColumn("Plugin"); - ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort); // Changes too fast to sort - ImGui.TableSetupColumn("Longest"); - ImGui.TableSetupColumn("Average"); - ImGui.TableHeadersRow(); + | ImGuiTableFlags.Hideable); - var sortSpecs = ImGui.TableGetSortSpecs(); - loadedPlugins = sortSpecs.Specs.ColumnIndex switch + if (table) { - 0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending - ? loadedPlugins.OrderBy(plugin => plugin.Name) - : loadedPlugins.OrderByDescending(plugin => plugin.Name), - 2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending - ? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0) - : loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0), - 3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending - ? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0) - : loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0), - _ => loadedPlugins, - }; + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("Plugin"); + ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort); // Changes too fast to sort + ImGui.TableSetupColumn("Longest"); + ImGui.TableSetupColumn("Average"); + ImGui.TableHeadersRow(); - foreach (var plugin in loadedPlugins) - { - if (!this.drawSearchText.IsNullOrEmpty() - && !plugin.Manifest.Name.Contains(this.drawSearchText, StringComparison.OrdinalIgnoreCase)) + var sortSpecs = ImGui.TableGetSortSpecs(); + loadedPlugins = sortSpecs.Specs.ColumnIndex switch { - continue; - } + 0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? loadedPlugins.OrderBy(plugin => plugin.Name) + : loadedPlugins.OrderByDescending(plugin => plugin.Name), + 2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0) + : loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0), + 3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0) + : loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0), + _ => loadedPlugins, + }; - ImGui.TableNextRow(); - - ImGui.TableNextColumn(); - ImGui.Text(plugin.Manifest.Name); - - if (plugin.DalamudInterface != null) + foreach (var plugin in loadedPlugins) { - ImGui.TableNextColumn(); - ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.LastDrawTime / 10000f:F4}ms"); + if (!this.drawSearchText.IsNullOrEmpty() + && !plugin.Manifest.Name.Contains(this.drawSearchText, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + ImGui.TableNextRow(); ImGui.TableNextColumn(); - ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime / 10000f:F4}ms"); + ImGui.Text(plugin.Manifest.Name); - ImGui.TableNextColumn(); - ImGui.Text(plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Count > 0 - ? $"{plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Average() / 10000f:F4}ms" - : "-"); + if (plugin.DalamudInterface != null) + { + ImGui.TableNextColumn(); + ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.LastDrawTime / 10000f:F4}ms"); + + ImGui.TableNextColumn(); + ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime / 10000f:F4}ms"); + + ImGui.TableNextColumn(); + ImGui.Text(plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Count > 0 + ? $"{plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Average() / 10000f:F4}ms" + : "-"); + } } } - - ImGui.EndTable(); } } - - ImGui.EndTabItem(); } - if (ImGui.BeginTabItem("Framework times")) + using (var tabItem = ImRaii.TabItem("Framework times")) { - var doStats = Framework.StatsEnabled; - - if (ImGui.Checkbox("Enable Framework Update Tracking", ref doStats)) + if (tabItem) { - Framework.StatsEnabled = doStats; - } + var doStats = Framework.StatsEnabled; - if (doStats) - { - ImGui.SameLine(); - if (ImGui.Button("Reset")) + if (ImGui.Checkbox("Enable Framework Update Tracking", ref doStats)) { - Framework.StatsHistory.Clear(); + Framework.StatsEnabled = doStats; } - var statsHistory = Framework.StatsHistory.ToArray(); - var totalLast = statsHistory.Sum(stats => stats.Value.LastOrDefault()); - var totalAverage = statsHistory.Sum(stats => stats.Value.DefaultIfEmpty().Average()); + if (doStats) + { + ImGui.SameLine(); + if (ImGui.Button("Reset")) + { + Framework.StatsHistory.Clear(); + } - ImGuiComponents.TextWithLabel("Total Last", $"{totalLast:F4}ms", "All last update times added together"); - ImGui.SameLine(); - ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage:F4}ms", "All average update times added together"); - ImGui.SameLine(); - ImGuiComponents.TextWithLabel("Collective Average", $"{(statsHistory.Any() ? totalAverage / statsHistory.Length : 0):F4}ms", "Average of all average update times"); + var statsHistory = Framework.StatsHistory.ToArray(); + var totalLast = statsHistory.Sum(stats => stats.Value.LastOrDefault()); + var totalAverage = statsHistory.Sum(stats => stats.Value.DefaultIfEmpty().Average()); - ImGui.InputTextWithHint( - "###PluginStatWindow_FrameworkSearch", - "Search", - ref this.frameworkSearchText, - 500); + ImGuiComponents.TextWithLabel("Total Last", $"{totalLast:F4}ms", "All last update times added together"); + ImGui.SameLine(); + ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage:F4}ms", "All average update times added together"); + ImGui.SameLine(); + ImGuiComponents.TextWithLabel("Collective Average", $"{(statsHistory.Any() ? totalAverage / statsHistory.Length : 0):F4}ms", "Average of all average update times"); - if (ImGui.BeginTable( + ImGui.InputTextWithHint( + "###PluginStatWindow_FrameworkSearch", + "Search", + ref this.frameworkSearchText, + 500); + + using var table = ImRaii.Table( "##PluginStatsFrameworkTimes", 4, ImGuiTableFlags.RowBg @@ -198,77 +203,77 @@ internal class PluginStatWindow : Window | ImGuiTableFlags.Resizable | ImGuiTableFlags.ScrollY | ImGuiTableFlags.Reorderable - | ImGuiTableFlags.Hideable)) - { - ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableSetupColumn("Method", ImGuiTableColumnFlags.None, 250); - ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort, 50); // Changes too fast to sort - ImGui.TableSetupColumn("Longest", ImGuiTableColumnFlags.None, 50); - ImGui.TableSetupColumn("Average", ImGuiTableColumnFlags.None, 50); - ImGui.TableHeadersRow(); - - var sortSpecs = ImGui.TableGetSortSpecs(); - statsHistory = sortSpecs.Specs.ColumnIndex switch + | ImGuiTableFlags.Hideable); + if (table) { - 0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending - ? statsHistory.OrderBy(handler => handler.Key).ToArray() - : statsHistory.OrderByDescending(handler => handler.Key).ToArray(), - 2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending - ? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Max()).ToArray() - : statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Max()).ToArray(), - 3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending - ? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Average()).ToArray() - : statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Average()).ToArray(), - _ => statsHistory, - }; + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("Method", ImGuiTableColumnFlags.None, 250); + ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort, 50); // Changes too fast to sort + ImGui.TableSetupColumn("Longest", ImGuiTableColumnFlags.None, 50); + ImGui.TableSetupColumn("Average", ImGuiTableColumnFlags.None, 50); + ImGui.TableHeadersRow(); - foreach (var handlerHistory in statsHistory) - { - if (!handlerHistory.Value.Any()) + var sortSpecs = ImGui.TableGetSortSpecs(); + statsHistory = sortSpecs.Specs.ColumnIndex switch { - continue; - } + 0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? statsHistory.OrderBy(handler => handler.Key).ToArray() + : statsHistory.OrderByDescending(handler => handler.Key).ToArray(), + 2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Max()).ToArray() + : statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Max()).ToArray(), + 3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Average()).ToArray() + : statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Average()).ToArray(), + _ => statsHistory, + }; - if (!this.frameworkSearchText.IsNullOrEmpty() - && handlerHistory.Key != null - && !handlerHistory.Key.Contains(this.frameworkSearchText, StringComparison.OrdinalIgnoreCase)) + foreach (var handlerHistory in statsHistory) { - continue; + if (!handlerHistory.Value.Any()) + { + continue; + } + + if (!this.frameworkSearchText.IsNullOrEmpty() + && handlerHistory.Key != null + && !handlerHistory.Key.Contains(this.frameworkSearchText, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + ImGui.Text($"{handlerHistory.Key}"); + + ImGui.TableNextColumn(); + ImGui.Text($"{handlerHistory.Value.Last():F4}ms"); + + ImGui.TableNextColumn(); + ImGui.Text($"{handlerHistory.Value.Max():F4}ms"); + + ImGui.TableNextColumn(); + ImGui.Text($"{handlerHistory.Value.Average():F4}ms"); } - - ImGui.TableNextRow(); - - ImGui.TableNextColumn(); - ImGui.Text($"{handlerHistory.Key}"); - - ImGui.TableNextColumn(); - ImGui.Text($"{handlerHistory.Value.Last():F4}ms"); - - ImGui.TableNextColumn(); - ImGui.Text($"{handlerHistory.Value.Max():F4}ms"); - - ImGui.TableNextColumn(); - ImGui.Text($"{handlerHistory.Value.Average():F4}ms"); } - - ImGui.EndTable(); } } - - ImGui.EndTabItem(); } - if (ImGui.BeginTabItem("Hooks")) + using (var tabItem = ImRaii.TabItem("Hooks")) { - ImGui.Checkbox("Show Dalamud Hooks", ref this.showDalamudHooks); + if (tabItem) + { + ImGui.Checkbox("Show Dalamud Hooks", ref this.showDalamudHooks); - ImGui.InputTextWithHint( - "###PluginStatWindow_HookSearch", - "Search", - ref this.hookSearchText, - 500); + ImGui.InputTextWithHint( + "###PluginStatWindow_HookSearch", + "Search", + ref this.hookSearchText, + 500); - if (ImGui.BeginTable( + using var table = ImRaii.Table( "##PluginStatsHooks", 4, ImGuiTableFlags.RowBg @@ -276,80 +281,78 @@ internal class PluginStatWindow : Window | ImGuiTableFlags.Resizable | ImGuiTableFlags.ScrollY | ImGuiTableFlags.Reorderable - | ImGuiTableFlags.Hideable)) - { - ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableSetupColumn("Detour Method", ImGuiTableColumnFlags.None, 250); - ImGui.TableSetupColumn("Address", ImGuiTableColumnFlags.None, 100); - ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.None, 40); - ImGui.TableSetupColumn("Backend", ImGuiTableColumnFlags.None, 40); - ImGui.TableHeadersRow(); - - foreach (var (guid, trackedHook) in HookManager.TrackedHooks) + | ImGuiTableFlags.Hideable); + if (table) { - try + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("Detour Method", ImGuiTableColumnFlags.None, 250); + ImGui.TableSetupColumn("Address", ImGuiTableColumnFlags.None, 100); + ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.None, 40); + ImGui.TableSetupColumn("Backend", ImGuiTableColumnFlags.None, 40); + ImGui.TableHeadersRow(); + + foreach (var (guid, trackedHook) in HookManager.TrackedHooks) { - if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly()) - continue; - - if (!this.hookSearchText.IsNullOrEmpty()) + try { - if ((trackedHook.Delegate.Target == null || !trackedHook.Delegate.Target.ToString().Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase)) - && !trackedHook.Delegate.Method.Name.Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase)) + if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly()) continue; - } - ImGui.TableNextRow(); - - ImGui.TableNextColumn(); - - ImGui.Text($"{trackedHook.Delegate.Target} :: {trackedHook.Delegate.Method.Name}"); - ImGui.TextDisabled(trackedHook.Assembly.FullName); - ImGui.TableNextColumn(); - if (!trackedHook.Hook.IsDisposed) - { - if (ImGui.Selectable($"{trackedHook.Hook.Address.ToInt64():X}")) + if (!this.hookSearchText.IsNullOrEmpty()) { - ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}"); - Service.Get().AddNotification($"{trackedHook.Hook.Address.ToInt64():X}", "Copied to clipboard", NotificationType.Success); + if ((trackedHook.Delegate.Target == null || !trackedHook.Delegate.Target.ToString().Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase)) + && !trackedHook.Delegate.Method.Name.Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase)) + continue; } - var processMemoryOffset = trackedHook.InProcessMemory; - if (processMemoryOffset.HasValue) + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + + ImGui.Text($"{trackedHook.Delegate.Target} :: {trackedHook.Delegate.Method.Name}"); + ImGui.TextDisabled(trackedHook.Assembly.FullName); + ImGui.TableNextColumn(); + if (!trackedHook.Hook.IsDisposed) { - if (ImGui.Selectable($"ffxiv_dx11.exe+{processMemoryOffset:X}")) + if (ImGui.Selectable($"{trackedHook.Hook.Address.ToInt64():X}")) { - ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}"); - Service.Get().AddNotification($"ffxiv_dx11.exe+{processMemoryOffset:X}", "Copied to clipboard", NotificationType.Success); + ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}"); + Service.Get().AddNotification($"{trackedHook.Hook.Address.ToInt64():X}", "Copied to clipboard", NotificationType.Success); + } + + var processMemoryOffset = trackedHook.InProcessMemory; + if (processMemoryOffset.HasValue) + { + if (ImGui.Selectable($"ffxiv_dx11.exe+{processMemoryOffset:X}")) + { + ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}"); + Service.Get().AddNotification($"ffxiv_dx11.exe+{processMemoryOffset:X}", "Copied to clipboard", NotificationType.Success); + } } } + + ImGui.TableNextColumn(); + + if (trackedHook.Hook.IsDisposed) + { + ImGui.Text("Disposed"); + } + else + { + ImGui.Text(trackedHook.Hook.IsEnabled ? "Enabled" : "Disabled"); + } + + ImGui.TableNextColumn(); + + ImGui.Text(trackedHook.Hook.BackendName); } - - ImGui.TableNextColumn(); - - if (trackedHook.Hook.IsDisposed) + catch (Exception ex) { - ImGui.Text("Disposed"); + Log.Error(ex, "Error drawing hooks in plugin stats"); } - else - { - ImGui.Text(trackedHook.Hook.IsEnabled ? "Enabled" : "Disabled"); - } - - ImGui.TableNextColumn(); - - ImGui.Text(trackedHook.Hook.BackendName); - } - catch (Exception ex) - { - Log.Error(ex, "Error drawing hooks in plugin stats"); } } - - ImGui.EndTable(); } } - - ImGui.EndTabBar(); } } diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index 4ca5df003..00f74fab7 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Numerics; @@ -54,6 +55,7 @@ internal class SelfTestWindow : Window new SheetRedirectResolverSelfTestStep(), new NounProcessorSelfTestStep(), new SeStringEvaluatorSelfTestStep(), + new CompletionSelfTestStep(), new LogoutEventSelfTestStep() ]; @@ -61,7 +63,7 @@ internal class SelfTestWindow : Window private bool selfTestRunning = false; private int currentStep = 0; - + private int scrollToStep = -1; private DateTimeOffset lastTestStart; /// @@ -90,9 +92,10 @@ internal class SelfTestWindow : Window if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward)) { - this.testIndexToResult.Add(this.currentStep, (SelfTestStepResult.NotRan, null)); + this.testIndexToResult[this.currentStep] = (SelfTestStepResult.NotRan, null); this.steps[this.currentStep].CleanUp(); this.currentStep++; + this.scrollToStep = this.currentStep; this.lastTestStart = DateTimeOffset.Now; if (this.currentStep >= this.steps.Count) @@ -107,6 +110,7 @@ internal class SelfTestWindow : Window { this.selfTestRunning = true; this.currentStep = 0; + this.scrollToStep = this.currentStep; this.testIndexToResult.Clear(); this.lastTestStart = DateTimeOffset.Now; } @@ -116,11 +120,11 @@ internal class SelfTestWindow : Window ImGui.TextUnformatted($"Step: {this.currentStep} / {this.steps.Count}"); - ImGuiHelpers.ScaledDummy(10); + ImGui.Spacing(); this.DrawResultTable(); - ImGuiHelpers.ScaledDummy(10); + ImGui.Spacing(); if (this.currentStep >= this.steps.Count) { @@ -131,11 +135,11 @@ internal class SelfTestWindow : Window if (this.testIndexToResult.Any(x => x.Value.Result == SelfTestStepResult.Fail)) { - ImGui.TextColored(ImGuiColors.DalamudRed, "One or more checks failed!"); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, "One or more checks failed!"); } else { - ImGui.TextColored(ImGuiColors.HealerGreen, "All checks passed!"); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.HealerGreen, "All checks passed!"); } return; @@ -146,7 +150,8 @@ internal class SelfTestWindow : Window return; } - ImGui.Separator(); + using var resultChild = ImRaii.Child("SelfTestResultChild", ImGui.GetContentRegionAvail()); + if (!resultChild) return; var step = this.steps[this.currentStep]; ImGui.TextUnformatted($"Current: {step.Name}"); @@ -164,13 +169,12 @@ internal class SelfTestWindow : Window result = SelfTestStepResult.Fail; } - ImGui.Separator(); - if (result != SelfTestStepResult.Waiting) { var duration = DateTimeOffset.Now - this.lastTestStart; - this.testIndexToResult.Add(this.currentStep, (result, duration)); + this.testIndexToResult[this.currentStep] = (result, duration); this.currentStep++; + this.scrollToStep = this.currentStep; this.lastTestStart = DateTimeOffset.Now; } @@ -178,90 +182,111 @@ internal class SelfTestWindow : Window private void DrawResultTable() { - if (ImGui.BeginTable("agingResultTable", 5, ImGuiTableFlags.Borders)) + var tableSize = ImGui.GetContentRegionAvail(); + + if (this.selfTestRunning) + tableSize -= new Vector2(0, 200); + + tableSize.Y = Math.Min(tableSize.Y, ImGui.GetWindowViewport().Size.Y * 0.5f); + + using var table = ImRaii.Table("agingResultTable", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, tableSize); + if (!table) + return; + + ImGui.TableSetupColumn("###index", ImGuiTableColumnFlags.WidthFixed, 12f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Name"); + ImGui.TableSetupColumn("Result", ImGuiTableColumnFlags.WidthFixed, 40f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Duration", ImGuiTableColumnFlags.WidthFixed, 90f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 30f * ImGuiHelpers.GlobalScale); + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); + + for (var i = 0; i < this.steps.Count; i++) { - ImGui.TableSetupColumn("###index", ImGuiTableColumnFlags.WidthFixed, 12f * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Name"); - ImGui.TableSetupColumn("Result", ImGuiTableColumnFlags.WidthFixed, 40f * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Duration", ImGuiTableColumnFlags.WidthFixed, 90f * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 30f * ImGuiHelpers.GlobalScale); + var step = this.steps[i]; + ImGui.TableNextRow(); - ImGui.TableHeadersRow(); - - for (var i = 0; i < this.steps.Count; i++) + if (this.selfTestRunning && this.currentStep == i) { - var step = this.steps[i]; - ImGui.TableNextRow(); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, ImGui.GetColorU32(ImGuiCol.TableRowBgAlt)); + } - ImGui.TableSetColumnIndex(0); - ImGui.Text(i.ToString()); + ImGui.TableSetColumnIndex(0); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(i.ToString()); - ImGui.TableSetColumnIndex(1); - ImGui.Text(step.Name); + if (this.selfTestRunning && this.scrollToStep == i) + { + ImGui.SetScrollHereY(); + this.scrollToStep = -1; + } - if (this.testIndexToResult.TryGetValue(i, out var result)) + ImGui.TableSetColumnIndex(1); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(step.Name); + + if (this.testIndexToResult.TryGetValue(i, out var result)) + { + ImGui.TableSetColumnIndex(2); + ImGui.AlignTextToFramePadding(); + + switch (result.Result) { - ImGui.TableSetColumnIndex(2); - ImGui.PushFont(InterfaceManager.MonoFont); + case SelfTestStepResult.Pass: + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.HealerGreen, "PASS"); + break; + case SelfTestStepResult.Fail: + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, "FAIL"); + break; + default: + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "NR"); + break; + } - switch (result.Result) - { - case SelfTestStepResult.Pass: - ImGui.TextColored(ImGuiColors.HealerGreen, "PASS"); - break; - case SelfTestStepResult.Fail: - ImGui.TextColored(ImGuiColors.DalamudRed, "FAIL"); - break; - default: - ImGui.TextColored(ImGuiColors.DalamudGrey, "NR"); - break; - } - - ImGui.PopFont(); - - ImGui.TableSetColumnIndex(3); - if (result.Duration.HasValue) - { - ImGui.TextUnformatted(result.Duration.Value.ToString("g")); - } + ImGui.TableSetColumnIndex(3); + if (result.Duration.HasValue) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(this.FormatTimeSpan(result.Duration.Value)); + } + } + else + { + ImGui.TableSetColumnIndex(2); + ImGui.AlignTextToFramePadding(); + if (this.selfTestRunning && this.currentStep == i) + { + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "WAIT"); } else { - ImGui.TableSetColumnIndex(2); - if (this.selfTestRunning && this.currentStep == i) - { - ImGui.TextColored(ImGuiColors.DalamudGrey, "WAIT"); - } - else - { - ImGui.TextColored(ImGuiColors.DalamudGrey, "NR"); - } - - ImGui.TableSetColumnIndex(3); - if (this.selfTestRunning && this.currentStep == i) - { - ImGui.TextUnformatted((DateTimeOffset.Now - this.lastTestStart).ToString("g")); - } + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "NR"); } - ImGui.TableSetColumnIndex(4); - using var id = ImRaii.PushId($"selfTest{i}"); - if (ImGuiComponents.IconButton(FontAwesomeIcon.FastForward)) + ImGui.TableSetColumnIndex(3); + ImGui.AlignTextToFramePadding(); + if (this.selfTestRunning && this.currentStep == i) { - this.StopTests(); - this.testIndexToResult.Remove(i); - this.currentStep = i; - this.selfTestRunning = true; - this.lastTestStart = DateTimeOffset.Now; - } - - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip("Jump to this test"); + ImGui.TextUnformatted(this.FormatTimeSpan(DateTimeOffset.Now - this.lastTestStart)); } } - ImGui.EndTable(); + ImGui.TableSetColumnIndex(4); + using var id = ImRaii.PushId($"selfTest{i}"); + if (ImGuiComponents.IconButton(FontAwesomeIcon.FastForward)) + { + this.StopTests(); + this.testIndexToResult.Remove(i); + this.currentStep = i; + this.selfTestRunning = true; + this.lastTestStart = DateTimeOffset.Now; + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Jump to this test"); + } } } @@ -281,4 +306,11 @@ internal class SelfTestWindow : Window } } } + + private string FormatTimeSpan(TimeSpan ts) + { + var str = ts.ToString("g", CultureInfo.InvariantCulture); + var commaPos = str.LastIndexOf('.'); + return commaPos > -1 && commaPos + 5 < str.Length ? str[..(commaPos + 5)] : str; + } } diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs new file mode 100644 index 000000000..40f8f3bce --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs @@ -0,0 +1,94 @@ +using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; + +/// +/// Test setup for Chat. +/// +internal class CompletionSelfTestStep : ISelfTestStep +{ + private int step = 0; + private bool registered; + private bool commandRun; + + /// + public string Name => "Test Completion"; + + /// + public SelfTestStepResult RunStep() + { + var cmdManager = Service.Get(); + switch (this.step) + { + case 0: + this.step++; + + break; + + case 1: + ImGui.Text("[Chat Log]"); + ImGui.TextWrapped("Use the category menus to navigate to [Dalamud], then complete a command from the list. Did it work?"); + if (ImGui.Button("Yes")) + this.step++; + ImGui.SameLine(); + + if (ImGui.Button("No")) + return SelfTestStepResult.Fail; + break; + case 2: + ImGui.Text("[Chat Log]"); + ImGui.Text("Type /xl into the chat log and tab-complete a dalamud command. Did it work?"); + + if (ImGui.Button("Yes")) + this.step++; + ImGui.SameLine(); + + if (ImGui.Button("No")) + return SelfTestStepResult.Fail; + + break; + + case 3: + ImGui.Text("[Chat Log]"); + if (!this.registered) + { + cmdManager.AddHandler("/xlselftestcompletion", new CommandInfo((_, _) => this.commandRun = true)); + this.registered = true; + } + + ImGui.Text("Tab-complete /xlselftestcompletion in the chat log and send the command"); + + if (this.commandRun) + this.step++; + + break; + + case 4: + ImGui.Text("[Other text inputs]"); + ImGui.Text("Open the party finder recruitment criteria dialog and try to tab-complete /xldev in the text box."); + ImGui.Text("Did the command appear in the text box? (It should not have)"); + if (ImGui.Button("Yes")) + return SelfTestStepResult.Fail; + ImGui.SameLine(); + + if (ImGui.Button("No")) + this.step++; + break; + case 5: + return SelfTestStepResult.Pass; + } + + return SelfTestStepResult.Waiting; + } + + /// + public void CleanUp() + { + Service.Get().RemoveHandler("/completionselftest"); + } +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs index a829a8bdc..d85e9958f 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs @@ -145,7 +145,7 @@ internal class ContextMenuSelfTestStep : ISelfTestStep var targetItem = (a.Target as MenuTargetInventory)!.TargetItem; if (targetItem is { } item) { - name = (this.itemSheet.GetRowOrDefault(item.ItemId)?.Name.ExtractText() ?? $"Unknown ({item.ItemId})") + (item.IsHq ? $" {SeIconChar.HighQuality.ToIconString()}" : string.Empty); + name = (this.itemSheet.GetRowOrDefault(item.BaseItemId)?.Name.ExtractText() ?? $"Unknown ({item.BaseItemId})") + (item.IsHq ? $" {SeIconChar.HighQuality.ToIconString()}" : string.Empty); count = item.Quantity; } else diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs index 3fbc4361f..ff9649a14 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs @@ -8,7 +8,7 @@ namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; /// Test setup for Lumina. /// /// ExcelRow to run test on. -/// Whether or not the sheet is large. If it is large, the self test will iterate through the full sheet in one frame and benchmark the time taken. +/// Whether the sheet is large. If it is large, the self test will iterate through the full sheet in one frame and benchmark the time taken. internal class LuminaSelfTestStep(bool isLargeSheet) : ISelfTestStep where T : struct, IExcelRow { diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs index d44467577..ac4a2a958 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Linq; using Dalamud.Bindings.ImGui; @@ -109,7 +109,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep } else { - ImGui.Text("Does this information match the purchase you made? This is testing the request to the server."); + ImGui.TextWrapped("Does this information match the purchase you made? This is testing the request to the server."); ImGui.Separator(); ImGui.Text($"Quantity: {this.marketBoardPurchaseRequest.ItemQuantity.ToString()}"); ImGui.Text($"Item ID: {this.marketBoardPurchaseRequest.CatalogId}"); @@ -135,7 +135,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep } else { - ImGui.Text("Does this information match the purchase you made? This is testing the response from the server."); + ImGui.TextWrapped("Does this information match the purchase you made? This is testing the response from the server."); ImGui.Separator(); ImGui.Text($"Quantity: {this.marketBoardPurchase.ItemQuantity.ToString()}"); ImGui.Text($"Item ID: {this.marketBoardPurchase.CatalogId}"); @@ -156,7 +156,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep case SubStep.Taxes: if (this.marketTaxRate == null) { - ImGui.Text("Goto a Retainer Vocate and talk to then. Click the 'View market tax rates' menu item."); + ImGui.TextWrapped("Goto a Retainer Vocate and talk to then. Click the 'View market tax rates' menu item."); } else { diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs index f28b658de..c7b85ead2 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs @@ -61,6 +61,10 @@ internal class SheetRedirectResolverSelfTestStep : ISelfTestStep new("WeatherPlaceName", 40), new("WeatherPlaceName", 52), new("WeatherPlaceName", 2300), + new("InstanceContent", 1), + new("PartyContent", 2), + new("PublicContent", 1), + new("AkatsukiNote", 1), ]; private unsafe delegate SheetRedirectFlags ResolveSheetRedirect(RaptureTextModule* thisPtr, Utf8String* sheetName, uint* rowId, uint* flags); diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs index 46013b72c..4cb239da3 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs @@ -11,12 +11,12 @@ public abstract class SettingsEntry public string? Name { get; protected set; } /// - /// Gets or sets a value indicating whether or not this entry is valid. + /// Gets or sets a value indicating whether this entry is valid. /// public virtual bool IsValid { get; protected set; } = true; /// - /// Gets or sets a value indicating whether or not this entry is visible. + /// Gets or sets a value indicating whether this entry is visible. /// public virtual bool IsVisible { get; protected set; } = true; @@ -54,7 +54,7 @@ public abstract class SettingsEntry { // ignored } - + /// /// Function to be called when the tab is closed. /// diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs index 699bfe448..df5cf5fd7 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs @@ -21,6 +21,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; public class SettingsTabAutoUpdates : SettingsTab { private AutoUpdateBehavior behavior; + private bool updateDisabledPlugins; private bool checkPeriodically; private bool chatNotification; private string pickerSearch = string.Empty; @@ -65,6 +66,7 @@ public class SettingsTabAutoUpdates : SettingsTab ImGuiHelpers.ScaledDummy(8); + ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdateDisabledPlugins", "Auto-Update plugins that are currently disabled"), ref this.updateDisabledPlugins); ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdateChatMessage", "Show notification about updates available in chat"), ref this.chatNotification); ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdatePeriodically", "Periodically check for new updates while playing"), ref this.checkPeriodically); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdatePeriodicallyHint", @@ -236,6 +238,7 @@ public class SettingsTabAutoUpdates : SettingsTab var configuration = Service.Get(); this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None; + this.updateDisabledPlugins = configuration.UpdateDisabledPlugins; this.chatNotification = configuration.SendUpdateNotificationToChat; this.checkPeriodically = configuration.CheckPeriodicallyForUpdates; this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences; @@ -248,6 +251,7 @@ public class SettingsTabAutoUpdates : SettingsTab var configuration = Service.Get(); configuration.AutoUpdateBehavior = this.behavior; + configuration.UpdateDisabledPlugins = this.updateDisabledPlugins; configuration.SendUpdateNotificationToChat = this.chatNotification; configuration.CheckPeriodicallyForUpdates = this.checkPeriodically; configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs index 1aae0dfb3..fae7e5e8f 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs @@ -60,7 +60,8 @@ public class SettingsTabExperimental : SettingsTab "Enable ImGui asserts"), Loc.Localize( "DalamudSettingEnableImGuiAssertsHint", - "If this setting is enabled, a window containing further details will be shown when an internal assertion in ImGui fails.\nWe recommend enabling this when developing plugins."), + "If this setting is enabled, a window containing further details will be shown when an internal assertion in ImGui fails.\nWe recommend enabling this when developing plugins. " + + "This setting does not persist and will reset when the game restarts.\nUse the setting below to enable it at startup."), c => Service.Get().ShowAsserts, (v, _) => Service.Get().ShowAsserts = v), diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 78ffdc514..513d99e00 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -12,6 +12,7 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ImGuiFontChooserDialog; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; using Dalamud.Interface.ManagedFontAtlas.Internals; @@ -38,7 +39,7 @@ public class SettingsTabLook : SettingsTab private IFontSpec defaultFontSpec = null!; public override SettingsEntry[] Entries { get; } = - { + [ new GapSettingsEntry(5, true), new ButtonSettingsEntry( @@ -46,6 +47,11 @@ public class SettingsTabLook : SettingsTab Loc.Localize("DalamudSettingsStyleEditorHint", "Modify the look & feel of Dalamud windows."), () => Service.Get().OpenStyleEditor()), + new ButtonSettingsEntry( + Loc.Localize("DalamudSettingsOpenNotificationEditor", "Modify Notification Position"), + Loc.Localize("DalamudSettingsNotificationEditorHint", "Choose where Dalamud notifications appear on the screen."), + () => Service.Get().StartPositionChooser()), + new SettingsEntry( Loc.Localize("DalamudSettingsUseDarkMode", "Use Windows immersive/dark mode"), Loc.Localize("DalamudSettingsUseDarkModeHint", "This will cause the FFXIV window title bar to follow your preferred Windows color settings, and switch to dark mode if enabled."), @@ -167,8 +173,8 @@ public class SettingsTabLook : SettingsTab ImGui.TextUnformatted("\uE020\uE021\uE022\uE023\uE024\uE025\uE026\uE027"); ImGui.PopStyleVar(1); }, - }, - }; + } + ]; public override string Title => Loc.Localize("DalamudSettingsVisual", "Look & Feel"); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs index fb46554d6..1a20593ec 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs @@ -51,7 +51,7 @@ public class DevPluginsSettingsEntry : SettingsEntry if (this.devPluginLocationsChanged) { - Service.Get().ScanDevPlugins(); + _ = Service.Get().ScanDevPluginsAsync(); this.devPluginLocationsChanged = false; } } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 1a5ff0eef..646004bb8 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -148,6 +148,10 @@ internal class TitleScreenMenuWindow : Window, IDisposable { ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0)); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0)); + + if (this.state == State.Show) + ImGui.SetNextWindowFocus(); + base.PreDraw(); } diff --git a/Dalamud/Interface/Style/StyleModelV1.cs b/Dalamud/Interface/Style/StyleModelV1.cs index 2197fd12f..8c1de86f3 100644 --- a/Dalamud/Interface/Style/StyleModelV1.cs +++ b/Dalamud/Interface/Style/StyleModelV1.cs @@ -101,8 +101,8 @@ public class StyleModelV1 : StyleModel { "DockingEmptyBg", new Vector4(0.2f, 0.2f, 0.2f, 1) }, { "PlotLines", new Vector4(0.61f, 0.61f, 0.61f, 1) }, { "PlotLinesHovered", new Vector4(1, 0.43f, 0.35f, 1) }, - { "PlotHistogram", new Vector4(0.9f, 0.7f, 0, 1) }, - { "PlotHistogramHovered", new Vector4(1, 0.6f, 0, 1) }, + { "PlotHistogram", new Vector4(0.578199f, 0.16989735f, 0.16989735f, 0.78431374f) }, + { "PlotHistogramHovered", new Vector4(0.7819905f, 0.12230185f, 0.12230185f, 0.78431374f) }, { "TableHeaderBg", new Vector4(0.19f, 0.19f, 0.2f, 1) }, { "TableBorderStrong", new Vector4(0.31f, 0.31f, 0.35f, 1) }, { "TableBorderLight", new Vector4(0.23f, 0.23f, 0.25f, 1) }, @@ -220,8 +220,8 @@ public class StyleModelV1 : StyleModel { "DockingEmptyBg", new Vector4(0.2f, 0.2f, 0.2f, 1f) }, { "PlotLines", new Vector4(0.61f, 0.61f, 0.61f, 1f) }, { "PlotLinesHovered", new Vector4(1f, 0.43f, 0.35f, 1f) }, - { "PlotHistogram", new Vector4(0.9f, 0.7f, 0f, 1f) }, - { "PlotHistogramHovered", new Vector4(1f, 0.6f, 0f, 1f) }, + { "PlotHistogram", new Vector4(0.578199f, 0.16989735f, 0.16989735f, 0.78431374f) }, + { "PlotHistogramHovered", new Vector4(0.7819905f, 0.12230185f, 0.12230185f, 0.78431374f) }, { "TableHeaderBg", new Vector4(0.19f, 0.19f, 0.2f, 1f) }, { "TableBorderStrong", new Vector4(0.31f, 0.31f, 0.35f, 1f) }, { "TableBorderLight", new Vector4(0.23f, 0.23f, 0.25f, 1f) }, diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs new file mode 100644 index 000000000..8a510e967 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs @@ -0,0 +1,498 @@ +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Memory; +using Dalamud.Utility; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.Textures.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + /// + public async Task CopyToClipboardAsync( + IDalamudTextureWrap wrap, + string? preferredFileNameWithoutExtension = null, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); + + using var wrapAux = new WrapAux(wrap, leaveWrapOpen); + bool hasAlphaChannel; + switch (wrapAux.Desc.Format) + { + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB: + hasAlphaChannel = false; + break; + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: + case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: + hasAlphaChannel = true; + break; + default: + await this.CopyToClipboardAsync( + await this.CreateFromExistingTextureAsync( + wrap, + new() { Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM }, + cancellationToken: cancellationToken), + preferredFileNameWithoutExtension, + false, + cancellationToken); + return; + } + + // https://stackoverflow.com/questions/15689541/win32-clipboard-and-alpha-channel-images + // https://learn.microsoft.com/en-us/windows/win32/shell/clipboard + using var pdo = default(ComPtr); + unsafe + { + fixed (Guid* piid = &IID.IID_IDataObject) + SHCreateDataObject(null, 1, null, null, piid, (void**)pdo.GetAddressOf()).ThrowOnError(); + } + + var ms = new MemoryStream(); + { + ms.SetLength(ms.Position = 0); + await this.SaveToStreamAsync( + wrap, + GUID.GUID_ContainerFormatPng, + ms, + new Dictionary { ["InterlaceOption"] = true }, + true, + true, + cancellationToken); + + unsafe + { + using var ims = default(ComPtr); + fixed (byte* p = ms.GetBuffer()) + ims.Attach(SHCreateMemStream(p, (uint)ms.Length)); + if (ims.IsEmpty()) + throw new OutOfMemoryException(); + + AddToDataObject( + pdo, + ClipboardFormats.Png, + new() + { + tymed = (uint)TYMED.TYMED_ISTREAM, + pstm = ims.Get(), + }); + AddToDataObject( + pdo, + ClipboardFormats.FileContents, + new() + { + tymed = (uint)TYMED.TYMED_ISTREAM, + pstm = ims.Get(), + }); + ims.Get()->AddRef(); + ims.Detach(); + } + + if (preferredFileNameWithoutExtension is not null) + { + unsafe + { + preferredFileNameWithoutExtension += ".png"; + if (preferredFileNameWithoutExtension.Length >= 260) + preferredFileNameWithoutExtension = preferredFileNameWithoutExtension[..^4] + ".png"; + var namea = (CodePagesEncodingProvider.Instance.GetEncoding(0) ?? Encoding.UTF8) + .GetBytes(preferredFileNameWithoutExtension); + if (namea.Length > 260) + { + namea.AsSpan()[^4..].CopyTo(namea.AsSpan(256, 4)); + Array.Resize(ref namea, 260); + } + + var fgda = new FILEGROUPDESCRIPTORA + { + cItems = 1, + fgd = new() + { + e0 = new() + { + dwFlags = unchecked((uint)FD_FLAGS.FD_FILESIZE | (uint)FD_FLAGS.FD_UNICODE), + nFileSizeHigh = (uint)(ms.Length >> 32), + nFileSizeLow = (uint)ms.Length, + }, + }, + }; + namea.AsSpan().CopyTo(new(fgda.fgd.e0.cFileName, 260)); + + AddToDataObject( + pdo, + ClipboardFormats.FileDescriptorA, + new() + { + tymed = (uint)TYMED.TYMED_HGLOBAL, + hGlobal = CreateHGlobalFromMemory(new(ref fgda)), + }); + + var fgdw = new FILEGROUPDESCRIPTORW + { + cItems = 1, + fgd = new() + { + e0 = new() + { + dwFlags = unchecked((uint)FD_FLAGS.FD_FILESIZE | (uint)FD_FLAGS.FD_UNICODE), + nFileSizeHigh = (uint)(ms.Length >> 32), + nFileSizeLow = (uint)ms.Length, + }, + }, + }; + preferredFileNameWithoutExtension.AsSpan().CopyTo(new(fgdw.fgd.e0.cFileName, 260)); + + AddToDataObject( + pdo, + ClipboardFormats.FileDescriptorW, + new() + { + tymed = (uint)TYMED.TYMED_HGLOBAL, + hGlobal = CreateHGlobalFromMemory(new(ref fgdw)), + }); + } + } + } + + { + ms.SetLength(ms.Position = 0); + await this.SaveToStreamAsync( + wrap, + GUID.GUID_ContainerFormatBmp, + ms, + new Dictionary { ["EnableV5Header32bppBGRA"] = false }, + true, + true, + cancellationToken); + AddToDataObject( + pdo, + CF.CF_DIB, + new() + { + tymed = (uint)TYMED.TYMED_HGLOBAL, + hGlobal = CreateHGlobalFromMemory( + ms.GetBuffer().AsSpan(0, (int)ms.Length)[Unsafe.SizeOf()..]), + }); + } + + if (hasAlphaChannel) + { + ms.SetLength(ms.Position = 0); + await this.SaveToStreamAsync( + wrap, + GUID.GUID_ContainerFormatBmp, + ms, + new Dictionary { ["EnableV5Header32bppBGRA"] = true }, + true, + true, + cancellationToken); + AddToDataObject( + pdo, + CF.CF_DIBV5, + new() + { + tymed = (uint)TYMED.TYMED_HGLOBAL, + hGlobal = CreateHGlobalFromMemory( + ms.GetBuffer().AsSpan(0, (int)ms.Length)[Unsafe.SizeOf()..]), + }); + } + + var omts = await Service.GetAsync(); + await omts.Run(() => StaThreadService.OleSetClipboard(pdo), cancellationToken); + + return; + + static unsafe void AddToDataObject(ComPtr pdo, uint clipboardFormat, STGMEDIUM stg) + { + var fec = new FORMATETC + { + cfFormat = (ushort)clipboardFormat, + ptd = null, + dwAspect = (uint)DVASPECT.DVASPECT_CONTENT, + lindex = 0, + tymed = stg.tymed, + }; + pdo.Get()->SetData(&fec, &stg, true).ThrowOnError(); + } + + static unsafe HGLOBAL CreateHGlobalFromMemory(ReadOnlySpan data) where T : unmanaged + { + var h = GlobalAlloc(GMEM.GMEM_MOVEABLE, (nuint)(data.Length * sizeof(T))); + if (h == 0) + throw new OutOfMemoryException("Failed to allocate."); + + var p = GlobalLock(h); + data.CopyTo(new(p, data.Length)); + GlobalUnlock(h); + return h; + } + } + + /// + public bool HasClipboardImage() + { + var acf = Service.Get().AvailableClipboardFormats; + return acf.Contains(CF.CF_DIBV5) + || acf.Contains(CF.CF_DIB) + || acf.Contains(ClipboardFormats.Png) + || acf.Contains(ClipboardFormats.FileContents); + } + + /// + public async Task CreateFromClipboardAsync( + string? debugName = null, + CancellationToken cancellationToken = default) + { + var omts = await Service.GetAsync(); + var (stgm, clipboardFormat) = await omts.Run(GetSupportedClipboardData, cancellationToken); + + try + { + return this.BlameSetName( + await this.DynamicPriorityTextureLoader.LoadAsync( + null, + ct => + clipboardFormat is CF.CF_DIB or CF.CF_DIBV5 + ? CreateTextureFromStorageMediumDib(this, stgm, ct) + : CreateTextureFromStorageMedium(this, stgm, ct), + cancellationToken), + debugName ?? $"{nameof(this.CreateFromClipboardAsync)}({(TYMED)stgm.tymed})"); + } + finally + { + StaThreadService.ReleaseStgMedium(ref stgm); + } + + // Converts a CF_DIB/V5 format to a full BMP format, for WIC consumption. + static unsafe Task CreateTextureFromStorageMediumDib( + TextureManager textureManager, + scoped in STGMEDIUM stgm, + CancellationToken ct) + { + var ms = new MemoryStream(); + switch ((TYMED)stgm.tymed) + { + case TYMED.TYMED_HGLOBAL when stgm.hGlobal != default: + { + var pMem = GlobalLock(stgm.hGlobal); + if (pMem is null) + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + try + { + var size = (int)GlobalSize(stgm.hGlobal); + ms.SetLength(sizeof(BITMAPFILEHEADER) + size); + new ReadOnlySpan(pMem, size).CopyTo(ms.GetBuffer().AsSpan(sizeof(BITMAPFILEHEADER))); + } + finally + { + GlobalUnlock(stgm.hGlobal); + } + + break; + } + + case TYMED.TYMED_ISTREAM when stgm.pstm is not null: + { + STATSTG stat; + if (stgm.pstm->Stat(&stat, (uint)STATFLAG.STATFLAG_NONAME).SUCCEEDED && stat.cbSize.QuadPart > 0) + ms.SetLength(sizeof(BITMAPFILEHEADER) + (int)stat.cbSize.QuadPart); + else + ms.SetLength(8192); + + var offset = (uint)sizeof(BITMAPFILEHEADER); + for (var read = 1u; read != 0;) + { + if (offset == ms.Length) + ms.SetLength(ms.Length * 2); + fixed (byte* pMem = ms.GetBuffer().AsSpan((int)offset)) + { + stgm.pstm->Read(pMem, (uint)(ms.Length - offset), &read).ThrowOnError(); + offset += read; + } + } + + ms.SetLength(offset); + break; + } + + default: + return Task.FromException(new NotSupportedException()); + } + + ref var bfh = ref Unsafe.As(ref ms.GetBuffer()[0]); + bfh.bfType = 0x4D42; + bfh.bfSize = (uint)ms.Length; + + ref var bih = ref Unsafe.As(ref ms.GetBuffer()[sizeof(BITMAPFILEHEADER)]); + bfh.bfOffBits = (uint)(sizeof(BITMAPFILEHEADER) + bih.biSize); + + if (bih.biSize >= sizeof(BITMAPINFOHEADER)) + { + if (bih.biBitCount > 8) + { + if (bih.biCompression == BI.BI_BITFIELDS) + bfh.bfOffBits += (uint)(3 * sizeof(RGBQUAD)); + else if (bih.biCompression == 6 /* BI_ALPHABITFIELDS */) + bfh.bfOffBits += (uint)(4 * sizeof(RGBQUAD)); + } + } + + if (bih.biClrUsed > 0) + bfh.bfOffBits += (uint)(bih.biClrUsed * sizeof(RGBQUAD)); + else if (bih.biBitCount <= 8) + bfh.bfOffBits += (uint)(sizeof(RGBQUAD) << bih.biBitCount); + + using var pinned = ms.GetBuffer().AsMemory().Pin(); + using var strm = textureManager.Wic.CreateIStreamViewOfMemory(pinned, (int)ms.Length); + return Task.FromResult(textureManager.Wic.NoThrottleCreateFromWicStream(strm, ct)); + } + + // Interprets a data as an image file using WIC. + static unsafe Task CreateTextureFromStorageMedium( + TextureManager textureManager, + scoped in STGMEDIUM stgm, + CancellationToken ct) + { + switch ((TYMED)stgm.tymed) + { + case TYMED.TYMED_HGLOBAL when stgm.hGlobal != default: + { + var pMem = GlobalLock(stgm.hGlobal); + if (pMem is null) + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + try + { + var size = (int)GlobalSize(stgm.hGlobal); + using var strm = textureManager.Wic.CreateIStreamViewOfMemory(pMem, size); + return Task.FromResult(textureManager.Wic.NoThrottleCreateFromWicStream(strm, ct)); + } + finally + { + GlobalUnlock(stgm.hGlobal); + } + } + + case TYMED.TYMED_FILE when stgm.lpszFileName is not null: + { + var fileName = MemoryHelper.ReadString((nint)stgm.lpszFileName, Encoding.Unicode, short.MaxValue); + return textureManager.NoThrottleCreateFromFileAsync(fileName, ct); + } + + case TYMED.TYMED_ISTREAM when stgm.pstm is not null: + { + using var strm = new ComPtr(stgm.pstm); + return Task.FromResult(textureManager.Wic.NoThrottleCreateFromWicStream(strm, ct)); + } + + default: + return Task.FromException(new NotSupportedException()); + } + } + + static unsafe bool TryGetClipboardDataAs( + ComPtr pdo, + uint clipboardFormat, + uint tymed, + out STGMEDIUM stgm) + { + var fec = new FORMATETC + { + cfFormat = (ushort)clipboardFormat, + ptd = null, + dwAspect = (uint)DVASPECT.DVASPECT_CONTENT, + lindex = -1, + tymed = tymed, + }; + fixed (STGMEDIUM* pstgm = &stgm) + return pdo.Get()->GetData(&fec, pstgm).SUCCEEDED; + } + + // Takes a data from clipboard for use with WIC. + static unsafe (STGMEDIUM Stgm, uint ClipboardFormat) GetSupportedClipboardData() + { + using var pdo = StaThreadService.OleGetClipboard(); + const uint tymeds = (uint)TYMED.TYMED_HGLOBAL | + (uint)TYMED.TYMED_FILE | + (uint)TYMED.TYMED_ISTREAM; + const uint sharedRead = STGM.STGM_READ | STGM.STGM_SHARE_DENY_WRITE; + + // Try taking data from clipboard as-is. + if (TryGetClipboardDataAs(pdo, CF.CF_DIBV5, tymeds, out var stgm)) + return (stgm, CF.CF_DIBV5); + if (TryGetClipboardDataAs(pdo, ClipboardFormats.FileContents, tymeds, out stgm)) + return (stgm, ClipboardFormats.FileContents); + if (TryGetClipboardDataAs(pdo, ClipboardFormats.Png, tymeds, out stgm)) + return (stgm, ClipboardFormats.Png); + if (TryGetClipboardDataAs(pdo, CF.CF_DIB, tymeds, out stgm)) + return (stgm, CF.CF_DIB); + + // Try reading file from the path stored in clipboard. + if (TryGetClipboardDataAs(pdo, ClipboardFormats.FileNameW, (uint)TYMED.TYMED_HGLOBAL, out stgm)) + { + var pPath = GlobalLock(stgm.hGlobal); + try + { + IStream* pfs; + SHCreateStreamOnFileW((ushort*)pPath, sharedRead, &pfs).ThrowOnError(); + + var stgm2 = new STGMEDIUM + { + tymed = (uint)TYMED.TYMED_ISTREAM, + pstm = pfs, + pUnkForRelease = (IUnknown*)pfs, + }; + return (stgm2, ClipboardFormats.FileContents); + } + finally + { + if (pPath is not null) + GlobalUnlock(stgm.hGlobal); + StaThreadService.ReleaseStgMedium(ref stgm); + } + } + + if (TryGetClipboardDataAs(pdo, ClipboardFormats.FileNameA, (uint)TYMED.TYMED_HGLOBAL, out stgm)) + { + var pPath = GlobalLock(stgm.hGlobal); + try + { + IStream* pfs; + SHCreateStreamOnFileA((sbyte*)pPath, sharedRead, &pfs).ThrowOnError(); + + var stgm2 = new STGMEDIUM + { + tymed = (uint)TYMED.TYMED_ISTREAM, + pstm = pfs, + pUnkForRelease = (IUnknown*)pfs, + }; + return (stgm2, ClipboardFormats.FileContents); + } + finally + { + if (pPath is not null) + GlobalUnlock(stgm.hGlobal); + StaThreadService.ReleaseStgMedium(ref stgm); + } + } + + throw new InvalidOperationException("No compatible clipboard format found."); + } + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index df84f9545..e7357a625 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -6,7 +6,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; @@ -173,7 +172,7 @@ internal sealed partial class TextureManager ReadOnlyMemory bytes, CancellationToken cancellationToken = default) { - ObjectDisposedException.ThrowIf(this.disposing, this); + ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); cancellationToken.ThrowIfCancellationRequested(); try @@ -204,7 +203,7 @@ internal sealed partial class TextureManager string path, CancellationToken cancellationToken = default) { - ObjectDisposedException.ThrowIf(this.disposing, this); + ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); cancellationToken.ThrowIfCancellationRequested(); try @@ -359,11 +358,18 @@ internal sealed partial class TextureManager /// An instance of . /// The number of bytes in the memory. /// The new instance of . - public unsafe ComPtr CreateIStreamViewOfMemory(MemoryHandle handle, int length) + public unsafe ComPtr CreateIStreamViewOfMemory(MemoryHandle handle, int length) => + this.CreateIStreamViewOfMemory((byte*)handle.Pointer, length); + + /// Creates a new instance of from a fixed memory allocation. + /// Address of the data. + /// The number of bytes in the memory. + /// The new instance of . + public unsafe ComPtr CreateIStreamViewOfMemory(void* address, int length) { using var wicStream = default(ComPtr); this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); - wicStream.Get()->InitializeFromMemory((byte*)handle.Pointer, checked((uint)length)).ThrowOnError(); + wicStream.Get()->InitializeFromMemory((byte*)address, checked((uint)length)).ThrowOnError(); var res = default(ComPtr); wicStream.As(ref res).ThrowOnError(); diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index e608956c2..059c716ce 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -11,7 +11,9 @@ using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; +using Dalamud.Storage.Assets; using Dalamud.Utility; using Dalamud.Utility.TerraFxCom; @@ -48,10 +50,11 @@ internal sealed partial class TextureManager [ServiceManager.ServiceDependency] private readonly InterfaceManager interfaceManager = Service.Get(); + private readonly CancellationTokenSource disposeCts = new(); + private DynamicPriorityQueueLoader? dynamicPriorityTextureLoader; private SharedTextureManager? sharedTextureManager; private WicManager? wicManager; - private bool disposing; private ComPtr device; [ServiceManager.ServiceConstructor] @@ -104,10 +107,10 @@ internal sealed partial class TextureManager /// void IInternalDisposableService.DisposeService() { - if (this.disposing) + if (this.disposeCts.IsCancellationRequested) return; - this.disposing = true; + this.disposeCts.Cancel(); Interlocked.Exchange(ref this.dynamicPriorityTextureLoader, null)?.Dispose(); Interlocked.Exchange(ref this.simpleDrawer, null)?.Dispose(); @@ -269,6 +272,21 @@ internal sealed partial class TextureManager return wrap; } + /// + public IDrawListTextureWrap CreateDrawListTexture(string? debugName = null) => this.CreateDrawListTexture(null, debugName); + + /// + /// Plugin that created the draw list. + /// + /// + public IDrawListTextureWrap CreateDrawListTexture(LocalPlugin? plugin, string? debugName = null) => + new DrawListTextureWrap( + new(this.device), + this, + Service.Get().Empty4X4, + plugin, + debugName ?? $"{nameof(this.CreateDrawListTexture)}"); + /// bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) => this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); @@ -330,7 +348,7 @@ internal sealed partial class TextureManager /// The loaded texture. internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) { - ObjectDisposedException.ThrowIf(this.disposing, this); + ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); var buffer = file.TextureBuffer; var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); @@ -354,7 +372,7 @@ internal sealed partial class TextureManager /// The loaded texture. internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan fileBytes) { - ObjectDisposedException.ThrowIf(this.disposing, this); + ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes)) throw new InvalidDataException("The file is not a TexFile."); diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs index c62ad61b4..93600a263 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs @@ -151,6 +151,10 @@ internal sealed class TextureManagerPluginScoped return textureWrap; } + /// + public IDrawListTextureWrap CreateDrawListTexture(string? debugName = null) => + this.ManagerOrThrow.CreateDrawListTexture(this.plugin, debugName); + /// public async Task CreateFromExistingTextureAsync( IDalamudTextureWrap wrap, @@ -267,6 +271,17 @@ internal sealed class TextureManagerPluginScoped return textureWrap; } + /// + public async Task CreateFromClipboardAsync( + string? debugName = null, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + var textureWrap = await manager.CreateFromClipboardAsync(debugName, cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + /// public IEnumerable GetSupportedImageDecoderInfos() => this.ManagerOrThrow.Wic.GetSupportedDecoderInfos(); @@ -279,6 +294,9 @@ internal sealed class TextureManagerPluginScoped return shared; } + /// + public bool HasClipboardImage() => this.ManagerOrThrow.HasClipboardImage(); + /// public bool TryGetFromGameIcon(in GameIconLookup lookup, [NotNullWhen(true)] out ISharedImmediateTexture? texture) { @@ -411,6 +429,17 @@ internal sealed class TextureManagerPluginScoped cancellationToken); } + /// + public async Task CopyToClipboardAsync( + IDalamudTextureWrap wrap, + string? preferredFileNameWithoutExtension = null, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + await manager.CopyToClipboardAsync(wrap, preferredFileNameWithoutExtension, leaveWrapOpen, cancellationToken); + } + private void ResultOnInterceptTexDataLoad(string path, ref string? replacementPath) => this.InterceptTexDataLoad?.Invoke(path, ref replacementPath); } diff --git a/Dalamud/Interface/Textures/TextureWraps/IDrawListTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/IDrawListTextureWrap.cs new file mode 100644 index 000000000..4c351258a --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/IDrawListTextureWrap.cs @@ -0,0 +1,60 @@ +using System.Numerics; + +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Textures.TextureWraps.Internal; + +namespace Dalamud.Interface.Textures.TextureWraps; + +/// A texture wrap that can be drawn using ImGui draw data. +public interface IDrawListTextureWrap : IDalamudTextureWrap +{ + /// Gets or sets the width of the texture. + /// If is to be set together, set use instead. + new int Width { get; set; } + + /// Gets or sets the width of the texture. + /// If is to be set together, set use instead. + new int Height { get; set; } + + /// Gets or sets the size of the texture. + /// Components will be rounded up. + new Vector2 Size { get; set; } + + /// + int IDalamudTextureWrap.Width => this.Width; + + /// + int IDalamudTextureWrap.Height => this.Height; + + /// + Vector2 IDalamudTextureWrap.Size => this.Size; + + /// Gets or sets the color to use when clearing this texture. + /// Color in RGBA. Defaults to , which is full transparency. + Vector4 ClearColor { get; set; } + + /// Draws a draw list to this texture. + /// Draw list to draw from. + /// Left-top coordinates of the draw commands in the draw list. + /// Scale to apply to all draw commands in the draw list. + /// This function can be called only from the main thread. + void Draw(ImDrawListPtr drawListPtr, Vector2 displayPos, Vector2 scale); + + /// + void Draw(scoped in ImDrawData drawData); + + /// Draws from a draw data to this texture. + /// Draw data to draw. + ///
    + ///
  • Texture size will be kept as specified in . will be + /// used only as shader parameters.
  • + ///
  • This function can be called only from the main thread.
  • + ///
+ void Draw(ImDrawDataPtr drawData); + + /// Resizes this texture and draws an ImGui window. + /// Name and ID of the window to draw. Use the value that goes into + /// . + /// Scale to apply to all draw commands in the draw list. + void ResizeAndDrawWindow(ReadOnlySpan windowName, Vector2 scale); +} diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs new file mode 100644 index 000000000..9f2ef9f78 --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs @@ -0,0 +1,302 @@ +using System.Numerics; + +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; +using Dalamud.Utility; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures.TextureWraps.Internal; + +/// +internal sealed unsafe partial class DrawListTextureWrap : IDrawListTextureWrap, IDeferredDisposable +{ + private readonly TextureManager textureManager; + private readonly IDalamudTextureWrap emptyTexture; + private readonly LocalPlugin? plugin; + private readonly string debugName; + + private ComPtr device; + private ComPtr deviceContext; + private ComPtr tex; + private ComPtr srv; + private ComPtr rtv; + + private ComPtr texPremultiplied; + private ComPtr srvPremultiplied; + private ComPtr rtvPremultiplied; + + private int width; + private int height; + private DXGI_FORMAT format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM; + + /// Initializes a new instance of the class. + /// Pointer to a D3D11 device. Ownership is taken. + /// Instance of the class. + /// Texture to use, if or is 0. + /// Plugin that holds responsible for this texture. + /// Name for debug display purposes. + public DrawListTextureWrap( + ComPtr device, + TextureManager textureManager, + IDalamudTextureWrap emptyTexture, + LocalPlugin? plugin, + string debugName) + { + this.textureManager = textureManager; + this.emptyTexture = emptyTexture; + this.plugin = plugin; + this.debugName = debugName; + + if (device.IsEmpty()) + throw new ArgumentNullException(nameof(device)); + + this.device.Swap(ref device); + fixed (ID3D11DeviceContext** pdc = &this.deviceContext.GetPinnableReference()) + this.device.Get()->GetImmediateContext(pdc); + + this.emptyTexture = emptyTexture; + this.srv = new((ID3D11ShaderResourceView*)emptyTexture.ImGuiHandle); + } + + /// Finalizes an instance of the class. + ~DrawListTextureWrap() => this.RealDispose(); + + /// + public ImTextureID Handle => new(this.srv.Get()); + + /// + public int Width + { + get => this.width; + set + { + ArgumentOutOfRangeException.ThrowIfNegative(value, nameof(value)); + this.Resize(value, this.height, this.format); + } + } + + /// + public int Height + { + get => this.height; + set + { + ArgumentOutOfRangeException.ThrowIfNegative(value, nameof(value)); + this.Resize(this.width, value, this.format).ThrowOnError(); + } + } + + /// + public Vector2 Size + { + get => new(this.width, this.height); + set + { + if (value.X is <= 0 or float.NaN) + throw new ArgumentOutOfRangeException(nameof(value), value, "X component is invalid."); + if (value.Y is <= 0 or float.NaN) + throw new ArgumentOutOfRangeException(nameof(value), value, "Y component is invalid."); + this.Resize((int)MathF.Ceiling(value.X), (int)MathF.Ceiling(value.Y), this.format).ThrowOnError(); + } + } + + /// + public Vector4 ClearColor { get; set; } + + /// Gets or sets the . + public int DxgiFormat + { + get => (int)this.format; + set + { + if (!this.textureManager.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)value)) + { + throw new ArgumentException( + "Specified format is not a supported rendering target format.", + nameof(value)); + } + + this.Resize(this.width, this.Height, (DXGI_FORMAT)value).ThrowOnError(); + } + } + + /// + public void Dispose() + { + if (Service.GetNullable() is { } im) + im.EnqueueDeferredDispose(this); + else + this.RealDispose(); + } + + /// + public void RealDispose() + { + this.srv.Reset(); + this.tex.Reset(); + this.rtv.Reset(); + this.srvPremultiplied.Reset(); + this.texPremultiplied.Reset(); + this.rtvPremultiplied.Reset(); + this.device.Reset(); + this.deviceContext.Reset(); + +#pragma warning disable CA1816 + GC.SuppressFinalize(this); +#pragma warning restore CA1816 + } + + /// + public void Draw(ImDrawListPtr drawListPtr, Vector2 displayPos, Vector2 scale) => + this.Draw( + new ImDrawData + { + Valid = 1, + CmdListsCount = 1, + TotalIdxCount = drawListPtr.IdxBuffer.Size, + TotalVtxCount = drawListPtr.VtxBuffer.Size, + CmdLists = (ImDrawList**)(&drawListPtr), + DisplayPos = displayPos, + DisplaySize = this.Size, + FramebufferScale = scale, + }); + + /// + public void Draw(scoped in ImDrawData drawData) + { + fixed (ImDrawData* pDrawData = &drawData) + this.Draw(new(pDrawData)); + } + + /// + public void Draw(ImDrawDataPtr drawData) + { + ThreadSafety.AssertMainThread(); + + // Do nothing if the render target is empty. + if (this.rtv.IsEmpty()) + return; + + // Clear the texture first, as the texture exists. + var clearColor = this.ClearColor; + this.deviceContext.Get()->ClearRenderTargetView(this.rtvPremultiplied.Get(), (float*)&clearColor); + + // If there is nothing to draw, then stop. + if (!drawData.Valid + || drawData.CmdListsCount < 1 + || drawData.TotalIdxCount < 1 + || drawData.TotalVtxCount < 1 + || drawData.CmdLists.IsNull + || drawData.DisplaySize.X <= 0 + || drawData.DisplaySize.Y <= 0 + || drawData.FramebufferScale.X == 0 + || drawData.FramebufferScale.Y == 0) + return; + + using (new DeviceContextStateBackup(this.device.Get()->GetFeatureLevel(), this.deviceContext)) + { + Service.Get().RenderDrawData(this.rtvPremultiplied.Get(), drawData); + Service.Get().MakeStraight(this.srvPremultiplied.Get(), this.rtv.Get()); + } + } + + /// Resizes the texture. + /// New texture width. + /// New texture height. + /// New format. + /// if the texture has been resized, if the texture has not + /// been resized, or a value with that evaluates to . + private HRESULT Resize(int newWidth, int newHeight, DXGI_FORMAT newFormat) + { + if (newWidth < 0 || newHeight < 0) + return E.E_INVALIDARG; + + if (newWidth == 0 || newHeight == 0) + { + this.tex.Reset(); + this.srv.Reset(); + this.rtv.Reset(); + this.texPremultiplied.Reset(); + this.srvPremultiplied.Reset(); + this.rtvPremultiplied.Reset(); + this.width = newWidth; + this.Height = newHeight; + this.srv = new((ID3D11ShaderResourceView*)this.emptyTexture.ImGuiHandle); + return S.S_FALSE; + } + + if (this.width == newWidth && this.height == newHeight) + return S.S_FALSE; + + // These new resources will take replace the existing resources, only once all allocations are completed. + using var tmptex = default(ComPtr); + using var tmpsrv = default(ComPtr); + using var tmprtv = default(ComPtr); + using var tmptexPremultiplied = default(ComPtr); + using var tmpsrvPremultiplied = default(ComPtr); + using var tmprtvPremultiplied = default(ComPtr); + + var tmpTexDesc = new D3D11_TEXTURE2D_DESC + { + Width = (uint)newWidth, + Height = (uint)newHeight, + MipLevels = 1, + ArraySize = 1, + Format = newFormat, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, + BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | + D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), + CPUAccessFlags = 0u, + MiscFlags = 0u, + }; + var hr = this.device.Get()->CreateTexture2D(&tmpTexDesc, null, tmptex.GetAddressOf()); + if (hr.FAILED) + return hr; + + var tmpres = (ID3D11Resource*)tmptex.Get(); + var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC(tmptex, D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + hr = this.device.Get()->CreateShaderResourceView(tmpres, &srvDesc, tmpsrv.GetAddressOf()); + if (hr.FAILED) + return hr; + + var rtvDesc = new D3D11_RENDER_TARGET_VIEW_DESC(tmptex, D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); + hr = this.device.Get()->CreateRenderTargetView(tmpres, &rtvDesc, tmprtv.GetAddressOf()); + if (hr.FAILED) + return hr; + + hr = this.device.Get()->CreateTexture2D(&tmpTexDesc, null, tmptexPremultiplied.GetAddressOf()); + if (hr.FAILED) + return hr; + + tmpres = (ID3D11Resource*)tmptexPremultiplied.Get(); + srvDesc = new(tmptexPremultiplied, D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + hr = this.device.Get()->CreateShaderResourceView(tmpres, &srvDesc, tmpsrvPremultiplied.GetAddressOf()); + if (hr.FAILED) + return hr; + + rtvDesc = new(tmptexPremultiplied, D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); + hr = this.device.Get()->CreateRenderTargetView(tmpres, &rtvDesc, tmprtvPremultiplied.GetAddressOf()); + if (hr.FAILED) + return hr; + + tmptex.Swap(ref this.tex); + tmpsrv.Swap(ref this.srv); + tmprtv.Swap(ref this.rtv); + tmptexPremultiplied.Swap(ref this.texPremultiplied); + tmpsrvPremultiplied.Swap(ref this.srvPremultiplied); + tmprtvPremultiplied.Swap(ref this.rtvPremultiplied); + this.width = newWidth; + this.height = newHeight; + this.format = newFormat; + + this.textureManager.BlameSetName(this, this.debugName); + this.textureManager.Blame(this, this.plugin); + return S.S_OK; + } +} diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/DeviceContextStateBackup.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/DeviceContextStateBackup.cs new file mode 100644 index 000000000..55cf13881 --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/DeviceContextStateBackup.cs @@ -0,0 +1,669 @@ +using System.Runtime.InteropServices; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures.TextureWraps.Internal; + +/// +internal sealed unsafe partial class DrawListTextureWrap +{ + /// Captures states of a . + // TODO: Use the one in https://github.com/goatcorp/Dalamud/pull/1923 once the PR goes in + internal struct DeviceContextStateBackup : IDisposable + { + private InputAssemblerState inputAssemblerState; + private RasterizerState rasterizerState; + private OutputMergerState outputMergerState; + private VertexShaderState vertexShaderState; + private HullShaderState hullShaderState; + private DomainShaderState domainShaderState; + private GeometryShaderState geometryShaderState; + private PixelShaderState pixelShaderState; + private ComputeShaderState computeShaderState; + + /// + /// Initializes a new instance of the struct, + /// by capturing all states of a . + /// + /// The feature level. + /// The device context. + public DeviceContextStateBackup(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx) + { + this.inputAssemblerState = InputAssemblerState.From(ctx); + this.rasterizerState = RasterizerState.From(ctx); + this.outputMergerState = OutputMergerState.From(featureLevel, ctx); + this.vertexShaderState = VertexShaderState.From(ctx); + this.hullShaderState = HullShaderState.From(ctx); + this.domainShaderState = DomainShaderState.From(ctx); + this.geometryShaderState = GeometryShaderState.From(ctx); + this.pixelShaderState = PixelShaderState.From(ctx); + this.computeShaderState = ComputeShaderState.From(featureLevel, ctx); + } + + /// + public void Dispose() + { + this.inputAssemblerState.Dispose(); + this.rasterizerState.Dispose(); + this.outputMergerState.Dispose(); + this.vertexShaderState.Dispose(); + this.hullShaderState.Dispose(); + this.domainShaderState.Dispose(); + this.geometryShaderState.Dispose(); + this.pixelShaderState.Dispose(); + this.computeShaderState.Dispose(); + } + + /// + /// Captures Input Assembler states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct InputAssemblerState : IDisposable + { + private const int BufferCount = D3D11.D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT; + + private ComPtr context; + private ComPtr layout; + private ComPtr indexBuffer; + private DXGI_FORMAT indexFormat; + private uint indexOffset; + private D3D_PRIMITIVE_TOPOLOGY topology; + private fixed ulong buffers[BufferCount]; + private fixed uint strides[BufferCount]; + private fixed uint offsets[BufferCount]; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static InputAssemblerState From(ID3D11DeviceContext* ctx) + { + var state = default(InputAssemblerState); + state.context.Attach(ctx); + ctx->AddRef(); + ctx->IAGetInputLayout(state.layout.GetAddressOf()); + ctx->IAGetPrimitiveTopology(&state.topology); + ctx->IAGetIndexBuffer(state.indexBuffer.GetAddressOf(), &state.indexFormat, &state.indexOffset); + ctx->IAGetVertexBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers, state.strides, state.offsets); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (InputAssemblerState* pThis = &this) + { + ctx->IASetInputLayout(pThis->layout); + ctx->IASetPrimitiveTopology(pThis->topology); + ctx->IASetIndexBuffer(pThis->indexBuffer, pThis->indexFormat, pThis->indexOffset); + ctx->IASetVertexBuffers( + 0, + BufferCount, + (ID3D11Buffer**)pThis->buffers, + pThis->strides, + pThis->offsets); + + pThis->context.Dispose(); + pThis->layout.Dispose(); + pThis->indexBuffer.Dispose(); + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + } + } + } + + /// + /// Captures Rasterizer states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct RasterizerState : IDisposable + { + private const int Count = D3D11.D3D11_VIEWPORT_AND_SCISSORRECT_MAX_INDEX; + + private ComPtr context; + private ComPtr state; + private fixed byte viewports[24 * Count]; + private fixed ulong scissorRects[16 * Count]; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static RasterizerState From(ID3D11DeviceContext* ctx) + { + var state = default(RasterizerState); + state.context.Attach(ctx); + ctx->AddRef(); + ctx->RSGetState(state.state.GetAddressOf()); + uint n = Count; + ctx->RSGetViewports(&n, (D3D11_VIEWPORT*)state.viewports); + n = Count; + ctx->RSGetScissorRects(&n, (RECT*)state.scissorRects); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (RasterizerState* pThis = &this) + { + ctx->RSSetState(pThis->state); + ctx->RSSetViewports(Count, (D3D11_VIEWPORT*)pThis->viewports); + ctx->RSSetScissorRects(Count, (RECT*)pThis->scissorRects); + + pThis->context.Dispose(); + pThis->state.Dispose(); + } + } + } + + /// + /// Captures Output Merger states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct OutputMergerState : IDisposable + { + private const int RtvCount = D3D11.D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT; + private const int UavCountMax = D3D11.D3D11_1_UAV_SLOT_COUNT; + + private ComPtr context; + private ComPtr blendState; + private fixed float blendFactor[4]; + private uint sampleMask; + private uint stencilRef; + private ComPtr depthStencilState; + private fixed ulong rtvs[RtvCount]; // ID3D11RenderTargetView*[RtvCount] + private ComPtr dsv; + private fixed ulong uavs[UavCountMax]; // ID3D11UnorderedAccessView*[UavCount] + private int uavCount; + + /// + /// Creates a new instance of from . + /// + /// The feature level. + /// The device context. + /// The captured state. + public static OutputMergerState From(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx) + { + var state = default(OutputMergerState); + state.uavCount = featureLevel >= D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1 + ? D3D11.D3D11_1_UAV_SLOT_COUNT + : D3D11.D3D11_PS_CS_UAV_REGISTER_COUNT; + state.context.Attach(ctx); + ctx->AddRef(); + ctx->OMGetBlendState(state.blendState.GetAddressOf(), state.blendFactor, &state.sampleMask); + ctx->OMGetDepthStencilState(state.depthStencilState.GetAddressOf(), &state.stencilRef); + ctx->OMGetRenderTargetsAndUnorderedAccessViews( + RtvCount, + (ID3D11RenderTargetView**)state.rtvs, + state.dsv.GetAddressOf(), + 0, + (uint)state.uavCount, + (ID3D11UnorderedAccessView**)state.uavs); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (OutputMergerState* pThis = &this) + { + ctx->OMSetBlendState(pThis->blendState, pThis->blendFactor, pThis->sampleMask); + ctx->OMSetDepthStencilState(pThis->depthStencilState, pThis->stencilRef); + var rtvc = (uint)RtvCount; + while (rtvc > 0 && pThis->rtvs[rtvc - 1] == 0) + rtvc--; + + var uavlb = rtvc; + while (uavlb < this.uavCount && pThis->uavs[uavlb] == 0) + uavlb++; + + var uavc = (uint)this.uavCount; + while (uavc > uavlb && pThis->uavs[uavc - 1] == 0) + uavlb--; + uavc -= uavlb; + + ctx->OMSetRenderTargetsAndUnorderedAccessViews( + rtvc, + (ID3D11RenderTargetView**)pThis->rtvs, + pThis->dsv, + uavc == 0 ? 0 : uavlb, + uavc, + uavc == 0 ? null : (ID3D11UnorderedAccessView**)pThis->uavs, + null); + + this.context.Reset(); + this.blendState.Reset(); + this.depthStencilState.Reset(); + this.dsv.Reset(); + foreach (ref var b in new Span>(pThis->rtvs, RtvCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->uavs, this.uavCount)) + b.Dispose(); + } + } + } + + /// + /// Captures Vertex Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct VertexShaderState : IDisposable + { + private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static VertexShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(VertexShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->VSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->VSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->VSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->VSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (VertexShaderState* pThis = &this) + { + ctx->VSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->VSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->VSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->VSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Hull Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct HullShaderState : IDisposable + { + private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static HullShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(HullShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->HSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->HSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->HSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->HSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (HullShaderState* pThis = &this) + { + ctx->HSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->HSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->HSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->HSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Domain Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct DomainShaderState : IDisposable + { + private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static DomainShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(DomainShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->DSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->DSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->DSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->DSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (DomainShaderState* pThis = &this) + { + ctx->DSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->DSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->DSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->DSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Geometry Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct GeometryShaderState : IDisposable + { + private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static GeometryShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(GeometryShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->GSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->GSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->GSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->GSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (GeometryShaderState* pThis = &this) + { + ctx->GSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->GSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->GSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->GSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Pixel Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct PixelShaderState : IDisposable + { + private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static PixelShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(PixelShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->PSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->PSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->PSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->PSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (PixelShaderState* pThis = &this) + { + ctx->PSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->PSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->PSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->PSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Compute Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct ComputeShaderState : IDisposable + { + private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int InstanceCount = 256; // According to msdn + private const int UavCountMax = D3D11.D3D11_1_UAV_SLOT_COUNT; + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[InstanceCount]; // ID3D11ClassInstance*[BufferCount] + private fixed ulong buffers[BufferCount]; // ID3D11Buffer*[BufferCount] + private fixed ulong samplers[SamplerCount]; // ID3D11SamplerState*[SamplerCount] + private fixed ulong resources[ResourceCount]; // ID3D11ShaderResourceView*[ResourceCount] + private fixed ulong uavs[UavCountMax]; // ID3D11UnorderedAccessView*[UavCountMax] + private uint instCount; + private int uavCount; + + /// + /// Creates a new instance of from . + /// + /// The feature level. + /// The device context. + /// The captured state. + public static ComputeShaderState From(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx) + { + var state = default(ComputeShaderState); + state.uavCount = featureLevel >= D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1 + ? D3D11.D3D11_1_UAV_SLOT_COUNT + : D3D11.D3D11_PS_CS_UAV_REGISTER_COUNT; + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = InstanceCount; + ctx->CSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->CSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->CSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->CSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + ctx->CSGetUnorderedAccessViews(0, (uint)state.uavCount, (ID3D11UnorderedAccessView**)state.uavs); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (ComputeShaderState* pThis = &this) + { + ctx->CSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->CSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->CSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->CSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + ctx->CSSetUnorderedAccessViews( + 0, + (uint)this.uavCount, + (ID3D11UnorderedAccessView**)pThis->uavs, + null); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->uavs, this.uavCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + } +} diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.Common.hlsl b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.Common.hlsl new file mode 100644 index 000000000..17b53ba6c --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.Common.hlsl @@ -0,0 +1,4 @@ +cbuffer TransformationBuffer : register(b0) { + float4x4 g_view; + float4 g_colorMultiplier; +} diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl new file mode 100644 index 000000000..b1cdf2fd0 --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl @@ -0,0 +1,39 @@ +#include "Renderer.Common.hlsl" + +struct ImDrawVert { + float2 position : POSITION; + float2 uv : TEXCOORD0; + float4 color : COLOR0; +}; + +struct VsData { + float4 position : SV_POSITION; + float2 uv : TEXCOORD0; + float4 color : COLOR0; +}; + +struct PsData { + float4 color : COLOR0; +}; + +Texture2D s_texture : register(t0); +SamplerState s_sampler : register(s0); + +VsData vs_main(const ImDrawVert idv) { + VsData result; + result.position = mul(g_view, float4(idv.position, 0, 1)); + result.uv = idv.uv; + result.color = idv.color; + return result; +} + +float4 ps_main(const VsData vd) : SV_TARGET { + return s_texture.Sample(s_sampler, vd.uv) * vd.color; +} + +/* + +fxc /Zi /T vs_5_0 /E vs_main /Fo Renderer.DrawToPremul.vs.bin Renderer.DrawToPremul.hlsl +fxc /Zi /T ps_5_0 /E ps_main /Fo Renderer.DrawToPremul.ps.bin Renderer.DrawToPremul.hlsl + +*/ diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.ps.bin b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.ps.bin new file mode 100644 index 000000000..80c297ce6 Binary files /dev/null and b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.ps.bin differ diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.vs.bin b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.vs.bin new file mode 100644 index 000000000..8549c1d65 Binary files /dev/null and b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.vs.bin differ diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl new file mode 100644 index 000000000..bce235d7f --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl @@ -0,0 +1,19 @@ +Texture2D s_texture : register(t0); + +float4 vs_main(const float2 position : POSITION) : SV_POSITION { + return float4(position, 0, 1); +} + +float4 ps_main(const float4 position : SV_POSITION) : SV_TARGET { + const float4 src = s_texture[position.xy]; + return src.a > 0 + ? float4(src.rgb / src.a, src.a) + : float4(0, 0, 0, 0); +} + +/* + +fxc /Zi /T vs_5_0 /E vs_main /Fo Renderer.MakeStraight.vs.bin Renderer.MakeStraight.hlsl +fxc /Zi /T ps_5_0 /E ps_main /Fo Renderer.MakeStraight.ps.bin Renderer.MakeStraight.hlsl + +*/ diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.ps.bin b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.ps.bin new file mode 100644 index 000000000..2892c7361 Binary files /dev/null and b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.ps.bin differ diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.vs.bin b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.vs.bin new file mode 100644 index 000000000..1bbe7e592 Binary files /dev/null and b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.vs.bin differ diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs new file mode 100644 index 000000000..06b580de3 --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs @@ -0,0 +1,595 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Reflection; +using System.Runtime.InteropServices; + +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures.TextureWraps.Internal; + +/// +internal sealed unsafe partial class DrawListTextureWrap +{ + /// The renderer. + [ServiceManager.EarlyLoadedService] + internal sealed class Renderer : IInternalDisposableService + { + private ComPtr device; + private ComPtr deviceContext; + + private ComPtr drawToPremulVertexShader; + private ComPtr drawToPremulPixelShader; + private ComPtr drawToPremulInputLayout; + private ComPtr drawToPremulVertexBuffer; + private ComPtr drawToPremulVertexConstantBuffer; + private ComPtr drawToPremulIndexBuffer; + + private ComPtr makeStraightVertexShader; + private ComPtr makeStraightPixelShader; + private ComPtr makeStraightInputLayout; + private ComPtr makeStraightVertexBuffer; + private ComPtr makeStraightIndexBuffer; + + private ComPtr samplerState; + private ComPtr blendState; + private ComPtr rasterizerState; + private ComPtr depthStencilState; + private int vertexBufferSize; + private int indexBufferSize; + + [ServiceManager.ServiceConstructor] + private Renderer(InterfaceManager.InterfaceManagerWithScene iwms) + { + try + { + this.device = new((ID3D11Device*)iwms.Manager.Backend!.DeviceHandle); + fixed (ID3D11DeviceContext** p = &this.deviceContext.GetPinnableReference()) + this.device.Get()->GetImmediateContext(p); + this.deviceContext.Get()->AddRef(); + + this.Setup(); + } + catch + { + this.ReleaseUnmanagedResources(); + throw; + } + } + + /// Finalizes an instance of the class. + ~Renderer() => this.ReleaseUnmanagedResources(); + + /// + public void DisposeService() => this.ReleaseUnmanagedResources(); + + /// Renders draw data. + /// The render target. + /// Pointer to the draw data. + public void RenderDrawData(ID3D11RenderTargetView* prtv, ImDrawDataPtr drawData) + { + ThreadSafety.AssertMainThread(); + + if (drawData.DisplaySize.X <= 0 || drawData.DisplaySize.Y <= 0 + || !drawData.Valid || drawData.CmdListsCount < 1) + return; + var cmdLists = new Span(drawData.CmdLists, drawData.CmdListsCount); + + // Create and grow vertex/index buffers if needed + if (this.vertexBufferSize < drawData.TotalVtxCount) + this.drawToPremulVertexBuffer.Dispose(); + if (this.drawToPremulVertexBuffer.Get() is null) + { + this.vertexBufferSize = drawData.TotalVtxCount + 5000; + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(ImDrawVert) * this.vertexBufferSize), + (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_DYNAMIC, + (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); + var buffer = default(ID3D11Buffer*); + this.device.Get()->CreateBuffer(&desc, null, &buffer).ThrowOnError(); + this.drawToPremulVertexBuffer.Attach(buffer); + } + + if (this.indexBufferSize < drawData.TotalIdxCount) + this.drawToPremulIndexBuffer.Dispose(); + if (this.drawToPremulIndexBuffer.Get() is null) + { + this.indexBufferSize = drawData.TotalIdxCount + 5000; + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(ushort) * this.indexBufferSize), + (uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_DYNAMIC, + (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); + var buffer = default(ID3D11Buffer*); + this.device.Get()->CreateBuffer(&desc, null, &buffer).ThrowOnError(); + this.drawToPremulIndexBuffer.Attach(buffer); + } + + // Upload vertex/index data into a single contiguous GPU buffer + try + { + var vertexData = default(D3D11_MAPPED_SUBRESOURCE); + var indexData = default(D3D11_MAPPED_SUBRESOURCE); + this.deviceContext.Get()->Map( + (ID3D11Resource*)this.drawToPremulVertexBuffer.Get(), + 0, + D3D11_MAP.D3D11_MAP_WRITE_DISCARD, + 0, + &vertexData).ThrowOnError(); + this.deviceContext.Get()->Map( + (ID3D11Resource*)this.drawToPremulIndexBuffer.Get(), + 0, + D3D11_MAP.D3D11_MAP_WRITE_DISCARD, + 0, + &indexData).ThrowOnError(); + + var targetVertices = new Span(vertexData.pData, this.vertexBufferSize); + var targetIndices = new Span(indexData.pData, this.indexBufferSize); + foreach (ref var cmdList in cmdLists) + { + var vertices = new ImVectorWrapper(cmdList.VtxBuffer.ToUntyped()); + var indices = new ImVectorWrapper(cmdList.IdxBuffer.ToUntyped()); + + vertices.DataSpan.CopyTo(targetVertices); + indices.DataSpan.CopyTo(targetIndices); + + targetVertices = targetVertices[vertices.Length..]; + targetIndices = targetIndices[indices.Length..]; + } + } + finally + { + this.deviceContext.Get()->Unmap((ID3D11Resource*)this.drawToPremulVertexBuffer.Get(), 0); + this.deviceContext.Get()->Unmap((ID3D11Resource*)this.drawToPremulIndexBuffer.Get(), 0); + } + + // Setup orthographic projection matrix into our constant buffer. + // Our visible imgui space lies from DisplayPos (LT) to DisplayPos+DisplaySize (RB). + // DisplayPos is (0,0) for single viewport apps. + try + { + var data = default(D3D11_MAPPED_SUBRESOURCE); + this.deviceContext.Get()->Map( + (ID3D11Resource*)this.drawToPremulVertexConstantBuffer.Get(), + 0, + D3D11_MAP.D3D11_MAP_WRITE_DISCARD, + 0, + &data).ThrowOnError(); + ref var xform = ref *(TransformationBuffer*)data.pData; + xform.View = + Matrix4x4.CreateOrthographicOffCenter( + drawData.DisplayPos.X, + drawData.DisplayPos.X + drawData.DisplaySize.X, + drawData.DisplayPos.Y + drawData.DisplaySize.Y, + drawData.DisplayPos.Y, + 1f, + 0f); + } + finally + { + this.deviceContext.Get()->Unmap((ID3D11Resource*)this.drawToPremulVertexConstantBuffer.Get(), 0); + } + + // Set up render state + { + this.deviceContext.Get()->IASetInputLayout(this.drawToPremulInputLayout); + var buffer = this.drawToPremulVertexBuffer.Get(); + var stride = (uint)sizeof(ImDrawVert); + var offset = 0u; + this.deviceContext.Get()->IASetVertexBuffers(0, 1, &buffer, &stride, &offset); + this.deviceContext.Get()->IASetIndexBuffer( + this.drawToPremulIndexBuffer, + DXGI_FORMAT.DXGI_FORMAT_R16_UINT, + 0); + this.deviceContext.Get()->IASetPrimitiveTopology( + D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + var viewport = new D3D11_VIEWPORT( + 0, + 0, + drawData.DisplaySize.X * drawData.FramebufferScale.X, + drawData.DisplaySize.Y * drawData.FramebufferScale.Y); + this.deviceContext.Get()->RSSetState(this.rasterizerState); + this.deviceContext.Get()->RSSetViewports(1, &viewport); + + var blendColor = default(Vector4); + this.deviceContext.Get()->OMSetBlendState(this.blendState, (float*)&blendColor, 0xffffffff); + this.deviceContext.Get()->OMSetDepthStencilState(this.depthStencilState, 0); + this.deviceContext.Get()->OMSetRenderTargets(1, &prtv, null); + + this.deviceContext.Get()->VSSetShader(this.drawToPremulVertexShader.Get(), null, 0); + buffer = this.drawToPremulVertexConstantBuffer.Get(); + this.deviceContext.Get()->VSSetConstantBuffers(0, 1, &buffer); + + // PS handled later + + this.deviceContext.Get()->GSSetShader(null, null, 0); + this.deviceContext.Get()->HSSetShader(null, null, 0); + this.deviceContext.Get()->DSSetShader(null, null, 0); + this.deviceContext.Get()->CSSetShader(null, null, 0); + } + + // Render command lists + // (Because we merged all buffers into a single one, we maintain our own offset into them) + var vertexOffset = 0; + var indexOffset = 0; + var clipOff = new Vector4(drawData.DisplayPos, drawData.DisplayPos.X, drawData.DisplayPos.Y); + var frameBufferScaleV4 = + new Vector4(drawData.FramebufferScale, drawData.FramebufferScale.X, drawData.FramebufferScale.Y); + foreach (ref var cmdList in cmdLists) + { + var cmds = new ImVectorWrapper(cmdList.CmdBuffer.ToUntyped()); + foreach (ref var cmd in cmds.DataSpan) + { + var clipV4 = (cmd.ClipRect - clipOff) * frameBufferScaleV4; + var clipRect = new RECT((int)clipV4.X, (int)clipV4.Y, (int)clipV4.Z, (int)clipV4.W); + + // Skip the draw if nothing would be visible + if (clipRect.left >= clipRect.right || clipRect.top >= clipRect.bottom || cmd.ElemCount == 0) + continue; + + this.deviceContext.Get()->RSSetScissorRects(1, &clipRect); + + if (cmd.UserCallback == null) + { + // Bind texture and draw + var samplerp = this.samplerState.Get(); + var srvp = (ID3D11ShaderResourceView*)cmd.TextureId.Handle; + this.deviceContext.Get()->PSSetShader(this.drawToPremulPixelShader, null, 0); + this.deviceContext.Get()->PSSetSamplers(0, 1, &samplerp); + this.deviceContext.Get()->PSSetShaderResources(0, 1, &srvp); + this.deviceContext.Get()->DrawIndexed( + cmd.ElemCount, + (uint)(cmd.IdxOffset + indexOffset), + (int)(cmd.VtxOffset + vertexOffset)); + } + } + + indexOffset += cmdList.IdxBuffer.Size; + vertexOffset += cmdList.VtxBuffer.Size; + } + } + + /// Renders draw data. + /// The pointer to a Texture2D SRV to read premultiplied data from. + /// The pointer to a Texture2D RTV to write straightened data. + public void MakeStraight(ID3D11ShaderResourceView* psrv, ID3D11RenderTargetView* prtv) + { + ThreadSafety.AssertMainThread(); + + D3D11_TEXTURE2D_DESC texDesc; + using (var texRes = default(ComPtr)) + { + prtv->GetResource(texRes.GetAddressOf()); + + using var tex = default(ComPtr); + texRes.As(&tex).ThrowOnError(); + tex.Get()->GetDesc(&texDesc); + } + + this.deviceContext.Get()->IASetInputLayout(this.makeStraightInputLayout); + var buffer = this.makeStraightVertexBuffer.Get(); + var stride = (uint)sizeof(Vector2); + var offset = 0u; + this.deviceContext.Get()->IASetVertexBuffers(0, 1, &buffer, &stride, &offset); + this.deviceContext.Get()->IASetIndexBuffer( + this.makeStraightIndexBuffer, + DXGI_FORMAT.DXGI_FORMAT_R16_UINT, + 0); + this.deviceContext.Get()->IASetPrimitiveTopology( + D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + var scissorRect = new RECT(0, 0, (int)texDesc.Width, (int)texDesc.Height); + this.deviceContext.Get()->RSSetScissorRects(1, &scissorRect); + this.deviceContext.Get()->RSSetState(this.rasterizerState); + var viewport = new D3D11_VIEWPORT(0, 0, texDesc.Width, texDesc.Height); + this.deviceContext.Get()->RSSetViewports(1, &viewport); + + this.deviceContext.Get()->OMSetBlendState(null, null, 0xffffffff); + this.deviceContext.Get()->OMSetDepthStencilState(this.depthStencilState, 0); + this.deviceContext.Get()->OMSetRenderTargets(1, &prtv, null); + + this.deviceContext.Get()->VSSetShader(this.makeStraightVertexShader.Get(), null, 0); + this.deviceContext.Get()->PSSetShader(this.makeStraightPixelShader.Get(), null, 0); + this.deviceContext.Get()->GSSetShader(null, null, 0); + this.deviceContext.Get()->HSSetShader(null, null, 0); + this.deviceContext.Get()->DSSetShader(null, null, 0); + this.deviceContext.Get()->CSSetShader(null, null, 0); + + this.deviceContext.Get()->PSSetShaderResources(0, 1, &psrv); + this.deviceContext.Get()->DrawIndexed(6, 0, 0); + } + + [SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed")] + private void Setup() + { + var assembly = Assembly.GetExecutingAssembly(); + var rendererName = typeof(Renderer).FullName!.Replace('+', '.'); + + if (this.drawToPremulVertexShader.IsEmpty() || this.drawToPremulInputLayout.IsEmpty()) + { + using var stream = assembly.GetManifestResourceStream($"{rendererName}.DrawToPremul.vs.bin")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + + using var tempShader = default(ComPtr); + using var tempInputLayout = default(ComPtr); + + fixed (byte* pArray = array) + fixed (void* pszPosition = "POSITION"u8) + fixed (void* pszTexCoord = "TEXCOORD"u8) + fixed (void* pszColor = "COLOR"u8) + { + this.device.Get()->CreateVertexShader( + pArray, + (nuint)stream.Length, + null, + tempShader.GetAddressOf()).ThrowOnError(); + + var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[] + { + new() + { + SemanticName = (sbyte*)pszPosition, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + new() + { + SemanticName = (sbyte*)pszTexCoord, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + new() + { + SemanticName = (sbyte*)pszColor, + Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, + AlignedByteOffset = uint.MaxValue, + }, + }; + this.device.Get()->CreateInputLayout( + ied, + 3, + pArray, + (nuint)stream.Length, + tempInputLayout.GetAddressOf()) + .ThrowOnError(); + } + + ArrayPool.Shared.Return(array); + + tempShader.Swap(ref this.drawToPremulVertexShader); + tempInputLayout.Swap(ref this.drawToPremulInputLayout); + } + + if (this.drawToPremulPixelShader.IsEmpty()) + { + using var stream = assembly.GetManifestResourceStream($"{rendererName}.DrawToPremul.ps.bin")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + + using var tmp = default(ComPtr); + fixed (byte* pArray = array) + { + this.device.Get()->CreatePixelShader(pArray, (nuint)stream.Length, null, tmp.GetAddressOf()) + .ThrowOnError(); + } + + ArrayPool.Shared.Return(array); + + tmp.Swap(ref this.drawToPremulPixelShader); + } + + if (this.makeStraightVertexShader.IsEmpty() || this.makeStraightInputLayout.IsEmpty()) + { + using var stream = assembly.GetManifestResourceStream($"{rendererName}.MakeStraight.vs.bin")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + + using var tempShader = default(ComPtr); + using var tempInputLayout = default(ComPtr); + + fixed (byte* pArray = array) + fixed (void* pszPosition = "POSITION"u8) + { + this.device.Get()->CreateVertexShader( + pArray, + (nuint)stream.Length, + null, + tempShader.GetAddressOf()).ThrowOnError(); + + var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[] + { + new() + { + SemanticName = (sbyte*)pszPosition, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + }; + this.device.Get()->CreateInputLayout( + ied, + 1, + pArray, + (nuint)stream.Length, + tempInputLayout.GetAddressOf()) + .ThrowOnError(); + } + + ArrayPool.Shared.Return(array); + + tempShader.Swap(ref this.makeStraightVertexShader); + tempInputLayout.Swap(ref this.makeStraightInputLayout); + } + + if (this.makeStraightPixelShader.IsEmpty()) + { + using var stream = assembly.GetManifestResourceStream($"{rendererName}.MakeStraight.ps.bin")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + + using var tmp = default(ComPtr); + fixed (byte* pArray = array) + { + this.device.Get()->CreatePixelShader(pArray, (nuint)stream.Length, null, tmp.GetAddressOf()) + .ThrowOnError(); + } + + ArrayPool.Shared.Return(array); + + tmp.Swap(ref this.makeStraightPixelShader); + } + + if (this.makeStraightVertexBuffer.IsEmpty()) + { + using var tmp = default(ComPtr); + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(Vector2) * 4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var data = stackalloc Vector2[] { new(-1, 1), new(-1, -1), new(1, 1), new(1, -1) }; + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; + this.device.Get()->CreateBuffer(&desc, &subr, tmp.GetAddressOf()).ThrowOnError(); + tmp.Swap(ref this.makeStraightVertexBuffer); + } + + if (this.makeStraightIndexBuffer.IsEmpty()) + { + using var tmp = default(ComPtr); + var desc = new D3D11_BUFFER_DESC( + sizeof(ushort) * 6, + (uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var data = stackalloc ushort[] { 0, 1, 2, 1, 2, 3 }; + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; + this.device.Get()->CreateBuffer(&desc, &subr, tmp.GetAddressOf()).ThrowOnError(); + tmp.Swap(ref this.makeStraightIndexBuffer); + } + + if (this.drawToPremulVertexConstantBuffer.IsEmpty()) + { + using var tmp = default(ComPtr); + var bufferDesc = new D3D11_BUFFER_DESC( + (uint)sizeof(TransformationBuffer), + (uint)D3D11_BIND_FLAG.D3D11_BIND_CONSTANT_BUFFER, + D3D11_USAGE.D3D11_USAGE_DYNAMIC, + (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); + this.device.Get()->CreateBuffer(&bufferDesc, null, tmp.GetAddressOf()).ThrowOnError(); + + tmp.Swap(ref this.drawToPremulVertexConstantBuffer); + } + + if (this.samplerState.IsEmpty()) + { + using var tmp = default(ComPtr); + var samplerDesc = new D3D11_SAMPLER_DESC + { + Filter = D3D11_FILTER.D3D11_FILTER_MIN_MAG_MIP_LINEAR, + AddressU = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + AddressV = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + AddressW = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + MipLODBias = 0, + MaxAnisotropy = 0, + ComparisonFunc = D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS, + MinLOD = 0, + MaxLOD = 0, + }; + this.device.Get()->CreateSamplerState(&samplerDesc, tmp.GetAddressOf()).ThrowOnError(); + + tmp.Swap(ref this.samplerState); + } + + // Create the blending setup + if (this.blendState.IsEmpty()) + { + using var tmp = default(ComPtr); + var blendStateDesc = new D3D11_BLEND_DESC + { + RenderTarget = + { + e0 = + { + BlendEnable = true, + SrcBlend = D3D11_BLEND.D3D11_BLEND_SRC_ALPHA, + DestBlend = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA, + BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_INV_DEST_ALPHA, + DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE, + BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALL, + }, + }, + }; + this.device.Get()->CreateBlendState(&blendStateDesc, tmp.GetAddressOf()).ThrowOnError(); + + tmp.Swap(ref this.blendState); + } + + // Create the rasterizer state + if (this.rasterizerState.IsEmpty()) + { + using var tmp = default(ComPtr); + var rasterizerDesc = new D3D11_RASTERIZER_DESC + { + FillMode = D3D11_FILL_MODE.D3D11_FILL_SOLID, + CullMode = D3D11_CULL_MODE.D3D11_CULL_NONE, + ScissorEnable = true, + DepthClipEnable = true, + }; + this.device.Get()->CreateRasterizerState(&rasterizerDesc, tmp.GetAddressOf()).ThrowOnError(); + + tmp.Swap(ref this.rasterizerState); + } + + // Create the depth-stencil State + if (this.depthStencilState.IsEmpty()) + { + using var tmp = default(ComPtr); + var dsDesc = new D3D11_DEPTH_STENCIL_DESC + { + DepthEnable = false, + StencilEnable = false, + }; + this.device.Get()->CreateDepthStencilState(&dsDesc, tmp.GetAddressOf()).ThrowOnError(); + + tmp.Swap(ref this.depthStencilState); + } + } + + private void ReleaseUnmanagedResources() + { + this.device.Reset(); + this.deviceContext.Reset(); + this.drawToPremulVertexShader.Reset(); + this.drawToPremulPixelShader.Reset(); + this.drawToPremulInputLayout.Reset(); + this.makeStraightVertexShader.Reset(); + this.makeStraightPixelShader.Reset(); + this.makeStraightInputLayout.Reset(); + this.samplerState.Reset(); + this.drawToPremulVertexConstantBuffer.Reset(); + this.blendState.Reset(); + this.rasterizerState.Reset(); + this.depthStencilState.Reset(); + this.drawToPremulVertexBuffer.Reset(); + this.drawToPremulIndexBuffer.Reset(); + } + + [StructLayout(LayoutKind.Sequential)] + private struct TransformationBuffer + { + public Matrix4x4 View; + public Vector4 ColorMultiplier; + } + } +} diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs new file mode 100644 index 000000000..e9a786a8c --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs @@ -0,0 +1,129 @@ +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +using Dalamud.Bindings.ImGui; + +namespace Dalamud.Interface.Textures.TextureWraps.Internal; + +/// +internal sealed unsafe partial class DrawListTextureWrap +{ + /// + public void ResizeAndDrawWindow(ReadOnlySpan windowName, Vector2 scale) + { + ref var window = ref ImGuiWindow.FindWindowByName(windowName); + if (Unsafe.IsNullRef(ref window)) + throw new ArgumentException("Window not found", nameof(windowName)); + + this.Size = window.Size; + + var numDrawList = CountDrawList(ref window); + var drawLists = stackalloc ImDrawList*[numDrawList]; + var drawData = new ImDrawData + { + Valid = 1, + CmdListsCount = numDrawList, + TotalIdxCount = 0, + TotalVtxCount = 0, + CmdLists = drawLists, + DisplayPos = window.Pos, + DisplaySize = window.Size, + FramebufferScale = scale, + }; + AddWindowToDrawData(ref window, ref drawLists); + for (var i = 0; i < numDrawList; i++) + { + drawData.TotalVtxCount += drawData.CmdLists[i]->VtxBuffer.Size; + drawData.TotalIdxCount += drawData.CmdLists[i]->IdxBuffer.Size; + } + + this.Draw(drawData); + + return; + + static bool IsWindowActiveAndVisible(scoped in ImGuiWindow window) => + window.Active != 0 && window.Hidden == 0; + + static void AddWindowToDrawData(scoped ref ImGuiWindow window, ref ImDrawList** wptr) + { + switch (window.DrawList.CmdBuffer.Size) + { + case 0: + case 1 when window.DrawList.CmdBuffer[0].ElemCount == 0 && + window.DrawList.CmdBuffer[0].UserCallback == null: + break; + default: + *wptr++ = window.DrawList; + break; + } + + for (var i = 0; i < window.DC.ChildWindows.Size; i++) + { + ref var child = ref *(ImGuiWindow*)window.DC.ChildWindows[i]; + if (IsWindowActiveAndVisible(in child)) // Clipped children may have been marked not active + AddWindowToDrawData(ref child, ref wptr); + } + } + + static int CountDrawList(scoped ref ImGuiWindow window) + { + var res = window.DrawList.CmdBuffer.Size switch + { + 0 => 0, + 1 when window.DrawList.CmdBuffer[0].ElemCount == 0 && + window.DrawList.CmdBuffer[0].UserCallback == null => 0, + _ => 1, + }; + for (var i = 0; i < window.DC.ChildWindows.Size; i++) + res += CountDrawList(ref *(ImGuiWindow*)window.DC.ChildWindows[i]); + return res; + } + } + + [StructLayout(LayoutKind.Explicit, Size = 0x448)] + private struct ImGuiWindow + { + [FieldOffset(0x048)] + public Vector2 Pos; + + [FieldOffset(0x050)] + public Vector2 Size; + + [FieldOffset(0x0CB)] + public byte Active; + + [FieldOffset(0x0D2)] + public byte Hidden; + + [FieldOffset(0x118)] + public ImGuiWindowTempData DC; + + [FieldOffset(0x2C0)] + public ImDrawListPtr DrawList; + + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] +#pragma warning disable SA1300 + public static extern ImGuiWindow* igCustom_FindWindowByName(byte* inherit); +#pragma warning restore SA1300 + + public static ref ImGuiWindow FindWindowByName(ReadOnlySpan name) + { + var nb = Encoding.UTF8.GetByteCount(name); + var buf = stackalloc byte[nb + 1]; + buf[Encoding.UTF8.GetBytes(name, new(buf, nb))] = 0; + + return ref *igCustom_FindWindowByName(buf); + } + + [StructLayout(LayoutKind.Explicit, Size = 0xF0)] + public struct ImGuiWindowTempData + { + [FieldOffset(0x98)] + public ImVector ChildWindows; + } + } +} diff --git a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs index a98d32770..1596db447 100644 --- a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs @@ -14,7 +14,7 @@ namespace Dalamud.Interface; public interface ITitleScreenMenuEntry : IReadOnlyTitleScreenMenuEntry, IComparable { /// - /// Gets or sets a value indicating whether or not this entry is internal. + /// Gets or sets a value indicating whether this entry is internal. /// bool IsInternal { get; set; } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index dbcda7022..9d0284ff6 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -153,7 +153,7 @@ public interface IUiBuilder bool DisableGposeUiHide { get; set; } /// - /// Gets or sets a value indicating whether or not the game's cursor should be overridden with the ImGui cursor. + /// Gets or sets a value indicating whether the game's cursor should be overridden with the ImGui cursor. /// bool OverrideGameCursor { get; set; } @@ -163,7 +163,7 @@ public interface IUiBuilder ulong FrameCount { get; } /// - /// Gets a value indicating whether or not a cutscene is playing. + /// Gets a value indicating whether a cutscene is playing. /// bool CutsceneActive { get; } @@ -183,7 +183,7 @@ public interface IUiBuilder IFontAtlas FontAtlas { get; } /// - /// Gets a value indicating whether or not to use "reduced motion". This usually means that you should use less + /// Gets a value indicating whether to use "reduced motion". This usually means that you should use less /// intrusive animations, or disable them entirely. /// bool ShouldUseReducedMotion { get; } @@ -503,7 +503,7 @@ public sealed class UiBuilder : IDisposable, IUiBuilder public bool DisableGposeUiHide { get; set; } = false; /// - /// Gets or sets a value indicating whether or not the game's cursor should be overridden with the ImGui cursor. + /// Gets or sets a value indicating whether the game's cursor should be overridden with the ImGui cursor. /// public bool OverrideGameCursor { @@ -517,7 +517,7 @@ public sealed class UiBuilder : IDisposable, IUiBuilder public ulong FrameCount { get; private set; } = 0; /// - /// Gets a value indicating whether or not a cutscene is playing. + /// Gets a value indicating whether a cutscene is playing. /// public bool CutsceneActive { @@ -547,7 +547,7 @@ public sealed class UiBuilder : IDisposable, IUiBuilder public IFontAtlas FontAtlas { get; } /// - /// Gets a value indicating whether or not to use "reduced motion". This usually means that you should use less + /// Gets a value indicating whether to use "reduced motion". This usually means that you should use less /// intrusive animations, or disable them entirely. /// public bool ShouldUseReducedMotion => Service.Get().ReduceMotions ?? false; diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index ec188e54c..c865817f4 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -470,7 +470,7 @@ public static partial class ImGuiHelpers /// /// Center the ImGui cursor for a certain text. - /// + /// /// The text to center for. public static void CenterCursorForText(string text) => CenterCursorFor(ImGui.CalcTextSize(text).X); @@ -481,6 +481,12 @@ public static partial class ImGuiHelpers public static void CenterCursorFor(float itemWidth) => ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2)); + /// + /// Starts a new horizontal button group. + /// + /// The group. + public static HorizontalButtonGroup BeginHorizontalButtonGroup() => new(); + /// /// Allocates memory on the heap using
/// Memory must be freed using . @@ -905,4 +911,162 @@ public static partial class ImGuiHelpers set => this.TextureIndexAndGlyphId = (this.TextureIndexAndGlyphId & ~GlyphIdMask) | ((uint)value << GlyphIdShift); } } + + /// + /// Class helper for creating a horizontal button group. + /// + public class HorizontalButtonGroup + { + private readonly List buttons = []; + + /// + /// Gets or sets a value indicating whether the buttons should be centered horizontally. + /// + public bool IsCentered { get; set; } = false; + + /// + /// Gets or sets the height of the buttons. If null, the default frame height is used. + /// + public float? Height { get; set; } + + /// + /// Gets or sets the extra margin to add to the inside of each button, before and after the text. + /// If null, the default margin is used. + /// + public float? ExtraMargin { get; set; } + + /// + /// Gets or sets the padding between buttons. If null, the default item spacing is used. + /// + public float? PaddingBetweenButtons { get; set; } + + /// + /// Add a button to the group. + /// + /// The text of the button. + /// The action to perform when the button is pressed. + /// The group. + public HorizontalButtonGroup Add(string text, Action action) + { + this.buttons.Add(new ButtonDef(text, action)); + return this; + } + + /// + /// Sets whether the buttons should be centered horizontally. + /// + /// The value. + /// The group. + public HorizontalButtonGroup SetCentered(bool centered) + { + this.IsCentered = centered; + return this; + } + + /// + /// Sets the height of the buttons. + /// + /// The height. + /// The group. + public HorizontalButtonGroup WithHeight(float height) + { + this.Height = height; + return this; + } + + /// + /// Sets the extra margin to add to the inside of each button, before and after the text. + /// + /// The margin. + /// The group. + public HorizontalButtonGroup WithExtraMargin(float extraMargin) + { + this.ExtraMargin = extraMargin; + return this; + } + + /// + /// Sets the padding between buttons. + /// + /// The padding. + /// The group. + public HorizontalButtonGroup WithPaddingBetweenButtons(float padding) + { + this.PaddingBetweenButtons = padding; + return this; + } + + /// + /// Draw the button group at the current location. + /// + public void Draw() + { + var buttonHeight = this.Height * GlobalScale ?? ImGui.GetFrameHeight(); + var buttonCount = this.buttons.Count; + + if (buttonCount == 0) + return; + + var buttonWidths = new float[buttonCount]; + var totalContentWidth = 0f; + var extraMargin = this.ExtraMargin ?? 0f; + + for (var i = 0; i < buttonCount; i++) + { + var buttonText = this.buttons[i].Text; + buttonWidths[i] = ImGui.CalcTextSize(buttonText).X + (2 * extraMargin) + (2 * ImGui.GetStyle().FramePadding.X); + totalContentWidth += buttonWidths[i]; + } + + var buttonPadding = this.PaddingBetweenButtons ?? ImGui.GetStyle().ItemSpacing.X; + if (buttonCount > 1) + totalContentWidth += buttonPadding * (buttonCount - 1); + + var startX = ImGui.GetCursorPosX(); + if (this.IsCentered) + { + var availWidth = ImGui.GetContentRegionAvail().X; + startX += (availWidth - totalContentWidth) * 0.5f; + ImGui.SetCursorPosX(startX); + } + + var originalSpacing = ImGui.GetStyle().ItemSpacing; + if (this.PaddingBetweenButtons.HasValue) + { + var spacing = originalSpacing; + spacing.X = this.PaddingBetweenButtons.Value; + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, spacing); + } + + for (var i = 0; i < buttonCount; i++) + { + var buttonDef = this.buttons[i]; + + if (this.ExtraMargin.HasValue) + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(ImGui.GetStyle().FramePadding.X + extraMargin, ImGui.GetStyle().FramePadding.Y)); + + if (this.Height.HasValue) + { + if (ImGui.Button(buttonDef.Text, new Vector2(buttonWidths[i], buttonHeight))) + buttonDef.Action?.Invoke(); + } + else + { + if (ImGui.Button(buttonDef.Text, new Vector2(buttonWidths[i], -1))) + buttonDef.Action?.Invoke(); + } + + if (this.ExtraMargin.HasValue) + ImGui.PopStyleVar(); + + if (i < buttonCount - 1) + ImGui.SameLine(); + } + + if (this.PaddingBetweenButtons.HasValue) + ImGui.PopStyleVar(); + } + + private record ButtonDef(string Text, Action Action); + } } diff --git a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs index 3318c870f..38b46d0c2 100644 --- a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs +++ b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text; using System.Threading.Tasks; using Dalamud.Bindings.ImGui; @@ -45,6 +46,19 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService string name, Task texture) { + name = new StringBuilder(name) + .Replace('<', '_') + .Replace('>', '_') + .Replace(':', '_') + .Replace('"', '_') + .Replace('/', '_') + .Replace('\\', '_') + .Replace('|', '_') + .Replace('?', '_') + .Replace('*', '_') + .ToString(); + + var isCopy = false; try { var initiatorScreenOffset = ImGui.GetMousePos(); @@ -52,11 +66,12 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService var textureManager = await Service.GetAsync(); var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.ImGuiHandle:X}"; - BitmapCodecInfo encoder; + BitmapCodecInfo? encoder; { var first = true; var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList(); - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var tcs = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); Service.Get().Draw += DrawChoices; encoder = await tcs.Task; @@ -82,6 +97,8 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService return; } + if (ImGui.Selectable("Copy")) + tcs.TrySetResult(null); foreach (var encoder2 in encoders) { if (ImGui.Selectable(encoder2.Name)) @@ -103,8 +120,21 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService } } - string path; + if (encoder is null) { + isCopy = true; + await textureManager.CopyToClipboardAsync(textureWrap, name, true); + } + else + { + var props = new Dictionary(); + if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) + props["CompressionQuality"] = 1.0f; + else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || + encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || + encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) + props["ImageQuality"] = 1.0f; + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); this.fileDialogManager.SaveFileDialog( "Save texture...", @@ -118,30 +148,23 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService else tcs.SetResult(path2); }); - path = await tcs.Task.ConfigureAwait(false); - } + var path = await tcs.Task.ConfigureAwait(false); - var props = new Dictionary(); - if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) - props["CompressionQuality"] = 1.0f; - else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || - encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || - encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) - props["ImageQuality"] = 1.0f; - await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props); + await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props); - var notif = Service.Get().AddNotification( - new() + var notif = Service.Get().AddNotification( + new() + { + Content = $"File saved to: {path}", + Title = initiatorName, + Type = NotificationType.Success, + }); + notif.Click += n => { - Content = $"File saved to: {path}", - Title = initiatorName, - Type = NotificationType.Success, - }); - notif.Click += n => - { - Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); - n.Notification.DismissNow(); - }; + Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); + n.Notification.DismissNow(); + }; + } } catch (Exception e) { @@ -152,7 +175,9 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService e, $"{nameof(DalamudInterface)}.{nameof(this.ShowTextureSaveMenuAsync)}({initiatorName}, {name})"); Service.Get().AddNotification( - $"Failed to save file: {e}", + isCopy + ? $"Failed to copy file: {e}" + : $"Failed to save file: {e}", initiatorName, NotificationType.Error); } diff --git a/Dalamud/Interface/Utility/Raii/EndObjects.cs b/Dalamud/Interface/Utility/Raii/EndObjects.cs index 4a6db846a..2ed87b5a4 100644 --- a/Dalamud/Interface/Utility/Raii/EndObjects.cs +++ b/Dalamud/Interface/Utility/Raii/EndObjects.cs @@ -65,6 +65,15 @@ public static partial class ImRaii public static IEndObject Combo(string label, string previewValue, ImGuiComboFlags flags) => new EndConditionally(ImGui.EndCombo, ImGui.BeginCombo(label, previewValue, flags)); + public static IEndObject Menu(string label) + => new EndConditionally(ImGui.EndMenu, ImGui.BeginMenu(label)); + + public static IEndObject MenuBar() + => new EndConditionally(ImGui.EndMenuBar, ImGui.BeginMenuBar()); + + public static IEndObject MainMenuBar() + => new EndConditionally(ImGui.EndMainMenuBar, ImGui.BeginMainMenuBar()); + public static IEndObject Group() { ImGui.BeginGroup(); diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index ddbd1bb3c..f770b0de9 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; +using System.Threading.Tasks; using CheapLoc; using Dalamud.Bindings.ImGui; @@ -11,7 +12,11 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; +using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Internal; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing.Persistence; using Dalamud.Logging.Internal; using Dalamud.Utility; @@ -25,6 +30,8 @@ namespace Dalamud.Interface.Windowing; ///
public abstract class Window { + private const float FadeInOutTime = 0.072f; + private static readonly ModuleLog Log = new("WindowSystem"); private static bool wasEscPressedLastFrame = false; @@ -41,6 +48,13 @@ public abstract class Window private PresetModel.PresetWindow? presetWindow; private bool presetDirty = false; + private bool pushedFadeInAlpha = false; + private float fadeInTimer = 0f; + private float fadeOutTimer = 0f; + private IDrawListTextureWrap? fadeOutTexture = null; + private Vector2 fadeOutSize = Vector2.Zero; + private Vector2 fadeOutOrigin = Vector2.Zero; + /// /// Initializes a new instance of the class. /// @@ -110,6 +124,11 @@ public abstract class Window /// Enable the built-in "additional options" menu on the title bar. /// UseAdditionalOptions = 1 << 2, + + /// + /// Do not draw non-critical animations. + /// + IsReducedMotion = 1 << 3, } /// @@ -149,6 +168,12 @@ public abstract class Window /// public uint OnCloseSfxId { get; set; } = 24u; + /// + /// Gets or sets a value indicating whether this window should not fade in and out, regardless of the users' + /// preference. + /// + public bool DisableFadeInFadeOut { get; set; } = false; + /// /// Gets or sets the position of this window. /// @@ -195,7 +220,7 @@ public abstract class Window public WindowSizeConstraints? SizeConstraints { get; set; } /// - /// Gets or sets a value indicating whether or not this window is collapsed. + /// Gets or sets a value indicating whether this window is collapsed. /// public bool? Collapsed { get; set; } @@ -210,7 +235,7 @@ public abstract class Window public ImGuiWindowFlags Flags { get; set; } /// - /// Gets or sets a value indicating whether or not this ImGui window will be forced to stay inside the main game window. + /// Gets or sets a value indicating whether this ImGui window will be forced to stay inside the main game window. /// public bool ForceMainWindow { get; set; } @@ -220,17 +245,17 @@ public abstract class Window public float? BgAlpha { get; set; } /// - /// Gets or sets a value indicating whether or not this ImGui window should display a close button in the title bar. + /// Gets or sets a value indicating whether this ImGui window should display a close button in the title bar. /// public bool ShowCloseButton { get; set; } = true; /// - /// Gets or sets a value indicating whether or not this window should offer to be pinned via the window's titlebar context menu. + /// Gets or sets a value indicating whether this window should offer to be pinned via the window's titlebar context menu. /// public bool AllowPinning { get; set; } = true; /// - /// Gets or sets a value indicating whether or not this window should offer to be made click-through via the window's titlebar context menu. + /// Gets or sets a value indicating whether this window should offer to be made click-through via the window's titlebar context menu. /// public bool AllowClickthrough { get; set; } = true; @@ -244,7 +269,7 @@ public abstract class Window public List TitleBarButtons { get; set; } = new(); /// - /// Gets or sets a value indicating whether or not this window will stay open. + /// Gets or sets a value indicating whether this window will stay open. /// public bool IsOpen { @@ -342,6 +367,14 @@ public abstract class Window { } + /// + /// Code to be executed when the window is safe to be disposed or removed from the window system. + /// Doing so in may result in animations not playing correctly. + /// + public virtual void OnSafeToRemove() + { + } + /// /// Code to be executed every frame, even when the window is collapsed. /// @@ -357,6 +390,7 @@ public abstract class Window internal void DrawInternal(WindowDrawFlags internalDrawFlags, WindowSystemPersistence? persistence) { this.PreOpenCheck(); + var doFades = !internalDrawFlags.HasFlag(WindowDrawFlags.IsReducedMotion) && !this.DisableFadeInFadeOut; if (!this.IsOpen) { @@ -371,9 +405,29 @@ public abstract class Window UIGlobals.PlaySoundEffect(this.OnCloseSfxId); } + if (this.fadeOutTexture != null) + { + this.fadeOutTimer -= ImGui.GetIO().DeltaTime; + if (this.fadeOutTimer <= 0f) + { + this.fadeOutTexture.Dispose(); + this.fadeOutTexture = null; + this.OnSafeToRemove(); + } + else + { + this.DrawFakeFadeOutWindow(); + } + } + + this.fadeInTimer = doFades ? 0f : FadeInOutTime; return; } + this.fadeInTimer += ImGui.GetIO().DeltaTime; + if (this.fadeInTimer > FadeInOutTime) + this.fadeInTimer = FadeInOutTime; + this.Update(); if (!this.DrawConditions()) return; @@ -457,6 +511,7 @@ public abstract class Window var showAdditions = (this.AllowPinning || this.AllowClickthrough) && internalDrawFlags.HasFlag(WindowDrawFlags.UseAdditionalOptions) && flagsApplicableForTitleBarIcons; + var printWindow = false; if (showAdditions) { ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f); @@ -531,6 +586,9 @@ public abstract class Window if (!isAvailable) ImGui.EndDisabled(); + if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window"))) + printWindow = true; + ImGui.EndPopup(); } @@ -588,8 +646,43 @@ public abstract class Window } } + this.fadeOutSize = ImGui.GetWindowSize(); + this.fadeOutOrigin = ImGui.GetWindowPos(); + var isCollapsed = ImGui.IsWindowCollapsed(); + var isDocked = ImGui.IsWindowDocked(); + ImGui.End(); + if (this.pushedFadeInAlpha) + { + ImGui.PopStyleVar(); + this.pushedFadeInAlpha = false; + } + + // TODO: No fade-out if the window is collapsed. We could do this if we knew the "FullSize" of the window + // from the internal ImGuiWindow, but I don't want to mess with that here for now. We can do this a lot + // easier with the new bindings. + // TODO: No fade-out if docking is enabled and the window is docked, since this makes them "unsnap". + // Ideally we should get rid of this "fake window" thing and just insert a new drawlist at the correct spot. + if (!this.internalIsOpen && this.fadeOutTexture == null && doFades && !isCollapsed && !isDocked) + { + this.fadeOutTexture = Service.Get().CreateDrawListTexture( + "WindowFadeOutTexture"); + this.fadeOutTexture.ResizeAndDrawWindow(this.WindowName, Vector2.One); + this.fadeOutTimer = FadeInOutTime; + } + + if (printWindow) + { + var tex = Service.Get().CreateDrawListTexture( + Loc.Localize("WindowSystemContextActionPrintWindow", "Print window")); + tex.ResizeAndDrawWindow(this.WindowName, Vector2.One); + _ = Service.Get().ShowTextureSaveMenuAsync( + this.WindowName, + this.WindowName, + Task.FromResult(tex)); + } + this.PostDraw(); this.PostHandlePreset(persistence); @@ -598,7 +691,7 @@ public abstract class Window ImGui.PopID(); } - private void ApplyConditionals() + private unsafe void ApplyConditionals() { if (this.Position.HasValue) { @@ -625,15 +718,20 @@ public abstract class Window ImGui.SetNextWindowSizeConstraints(this.SizeConstraints.Value.MinimumSize * ImGuiHelpers.GlobalScale, this.SizeConstraints.Value.MaximumSize * ImGuiHelpers.GlobalScale); } - if (this.BgAlpha.HasValue) + var maxBgAlpha = this.internalAlpha ?? this.BgAlpha; + var fadeInAlpha = this.fadeInTimer / FadeInOutTime; + if (fadeInAlpha < 1f) { - ImGui.SetNextWindowBgAlpha(this.BgAlpha.Value); + maxBgAlpha = maxBgAlpha.HasValue ? + Math.Clamp(maxBgAlpha.Value * fadeInAlpha, 0f, 1f) : + (*ImGui.GetStyleColorVec4(ImGuiCol.WindowBg)).W * fadeInAlpha; + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * fadeInAlpha); + this.pushedFadeInAlpha = true; } - // Manually set alpha takes precedence, if devs don't want that, they should turn it off - if (this.internalAlpha.HasValue) + if (maxBgAlpha.HasValue) { - ImGui.SetNextWindowBgAlpha(this.internalAlpha.Value); + ImGui.SetNextWindowBgAlpha(maxBgAlpha.Value); } } @@ -780,6 +878,35 @@ public abstract class Window ImGui.PopClipRect(); } + private void DrawFakeFadeOutWindow() + { + // Draw a fake window to fade out, so that the fade out texture stays in the right place in the + // focus order + ImGui.SetNextWindowPos(this.fadeOutOrigin); + ImGui.SetNextWindowSize(this.fadeOutSize); + + using var style = ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, Vector2.Zero); + style.Push(ImGuiStyleVar.WindowBorderSize, 0); + style.Push(ImGuiStyleVar.FrameBorderSize, 0); + + const ImGuiWindowFlags flags = ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs | + ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoBackground; + if (ImGui.Begin(this.WindowName, flags)) + { + var dl = ImGui.GetWindowDrawList(); + dl.AddImage( + this.fadeOutTexture!.Handle, + this.fadeOutOrigin, + this.fadeOutOrigin + this.fadeOutSize, + Vector2.Zero, + Vector2.One, + ImGui.ColorConvertFloat4ToU32(new(1f, 1f, 1f, Math.Clamp(this.fadeOutTimer / FadeInOutTime, 0f, 1f)))); + } + + ImGui.End(); + } + /// /// Structure detailing the size constraints of a window. /// @@ -853,7 +980,7 @@ public abstract class Window public int Priority { get; set; } /// - /// Gets or sets a value indicating whether or not the button shall be clickable + /// Gets or sets a value indicating whether the button shall be clickable /// when the respective window is set to clickthrough. /// public bool AvailableClickthrough { get; set; } diff --git a/Dalamud/Interface/Windowing/WindowSystem.cs b/Dalamud/Interface/Windowing/WindowSystem.cs index 3f881d89d..87bd199a1 100644 --- a/Dalamud/Interface/Windowing/WindowSystem.cs +++ b/Dalamud/Interface/Windowing/WindowSystem.cs @@ -118,6 +118,9 @@ public class WindowSystem if (config?.IsFocusManagementEnabled ?? false) flags |= Window.WindowDrawFlags.UseFocusManagement; + if (config?.ReduceMotions ?? false) + flags |= Window.WindowDrawFlags.IsReducedMotion; + // Shallow clone the list of windows so that we can edit it without modifying it while the loop is iterating foreach (var window in this.windows.ToArray()) { diff --git a/Dalamud/IoC/Internal/ObjectInstance.cs b/Dalamud/IoC/Internal/ObjectInstance.cs index 3fd626a05..3a963f6bd 100644 --- a/Dalamud/IoC/Internal/ObjectInstance.cs +++ b/Dalamud/IoC/Internal/ObjectInstance.cs @@ -13,9 +13,11 @@ internal class ObjectInstance /// /// Weak reference to the underlying instance. /// Type of the underlying instance. - public ObjectInstance(Task instanceTask, Type type) + /// The visibility of this instance. + public ObjectInstance(Task instanceTask, Type type, ObjectInstanceVisibility visibility) { this.InstanceTask = instanceTask; + this.Visibility = visibility; } /// @@ -23,4 +25,9 @@ internal class ObjectInstance /// /// The underlying instance. public Task InstanceTask { get; } + + /// + /// Gets or sets the visibility of the object instance. + /// + public ObjectInstanceVisibility Visibility { get; set; } } diff --git a/Dalamud/IoC/Internal/ObjectInstanceVisibility.cs b/Dalamud/IoC/Internal/ObjectInstanceVisibility.cs new file mode 100644 index 000000000..7ab564603 --- /dev/null +++ b/Dalamud/IoC/Internal/ObjectInstanceVisibility.cs @@ -0,0 +1,17 @@ +namespace Dalamud.IoC.Internal; + +/// +/// Enum that declares the visibility of an object instance in the service container. +/// +internal enum ObjectInstanceVisibility +{ + /// + /// The object instance is only visible to other internal services. + /// + Internal, + + /// + /// The object instance is visible to all services and plugins. + /// + ExposedToPlugins, +} diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index a8eacb02d..6745155f6 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.Design; using System.Diagnostics; using System.Linq; using System.Reflection; @@ -12,7 +13,7 @@ namespace Dalamud.IoC.Internal; /// /// A simple singleton-only IOC container that provides (optional) version-based dependency resolution. -/// +/// /// This is only used to resolve dependencies for plugins. /// Dalamud services are constructed via Service{T}.ConstructObject at the moment. /// @@ -29,13 +30,18 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// public ServiceContainer() { + // Register the service container itself as a singleton. + // For all other services, this is done through the static constructor of Service{T}. + this.instances.Add( + typeof(IServiceContainer), + new(new Task(() => new WeakReference(this), TaskCreationOptions.RunContinuationsAsynchronously), typeof(ServiceContainer), ObjectInstanceVisibility.Internal)); } - + /// /// Gets a dictionary of all registered instances. /// public IReadOnlyDictionary Instances => this.instances; - + /// /// Gets a dictionary mapping interfaces to their implementations. /// @@ -45,15 +51,13 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// Register a singleton object of any type into the current IOC container. /// /// The existing instance to register in the container. + /// The visibility of this singleton. /// The type to register. - public void RegisterSingleton(Task instance) + public void RegisterSingleton(Task instance, ObjectInstanceVisibility visibility) { - if (instance == null) - { - throw new ArgumentNullException(nameof(instance)); - } + ArgumentNullException.ThrowIfNull(instance); - this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T)); + this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T), visibility); } /// @@ -69,7 +73,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType foreach (var resolvableType in resolveViaTypes) { Log.Verbose("=> {InterfaceName} provides for {TName}", resolvableType.FullName ?? "???", type.FullName ?? "???"); - + Debug.Assert(!this.interfaceToTypeMap.ContainsKey(resolvableType), "A service already implements this interface, this is not allowed"); Debug.Assert(type.IsAssignableTo(resolvableType), "Service does not inherit from indicated ResolveVia type"); @@ -81,10 +85,11 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// Create an object. /// /// The type of object to create. + /// Defines which services are allowed to be directly resolved into this type. /// Scoped objects to be included in the constructor. /// The scope to be used to create scoped services. /// The created object. - public async Task CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null) + public async Task CreateAsync(Type objectType, ObjectInstanceVisibility allowedVisibility, object[] scopedObjects, IServiceScope? scope = null) { var errorStep = "constructor lookup"; @@ -174,7 +179,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType { if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType)) serviceType = implementingType; - + if (serviceType.GetCustomAttribute() != null) { if (scope == null) @@ -211,7 +216,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType private ConstructorInfo? FindApplicableCtor(Type type, object[] scopedObjects) { // get a list of all the available types: scoped and singleton - var types = scopedObjects + var allValidServiceTypes = scopedObjects .Select(o => o.GetType()) .Union(this.instances.Keys) .ToArray(); @@ -224,7 +229,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType var ctors = type.GetConstructors(ctorFlags); foreach (var ctor in ctors) { - if (this.ValidateCtor(ctor, types)) + if (this.ValidateCtor(ctor, allValidServiceTypes)) { return ctor; } @@ -233,28 +238,30 @@ internal class ServiceContainer : IServiceProvider, IServiceType return null; } - private bool ValidateCtor(ConstructorInfo ctor, Type[] types) + private bool ValidateCtor(ConstructorInfo ctor, Type[] validTypes) { bool IsTypeValid(Type type) { - var contains = types.Any(x => x.IsAssignableTo(type)); + var contains = validTypes.Any(x => x.IsAssignableTo(type)); // Scoped services are created on-demand return contains || type.GetCustomAttribute() != null; } - + var parameters = ctor.GetParameters(); foreach (var parameter in parameters) { var valid = IsTypeValid(parameter.ParameterType); - + // If this service is provided by an interface if (!valid && this.interfaceToTypeMap.TryGetValue(parameter.ParameterType, out var implementationType)) valid = IsTypeValid(implementationType); if (!valid) { - Log.Error("Failed to validate {TypeName}, unable to find any services that satisfy the type", parameter.ParameterType.FullName!); + Log.Error("Ctor from {DeclaringType}: Failed to validate {TypeName}, unable to find any services that satisfy the type", + ctor.DeclaringType?.FullName ?? ctor.DeclaringType?.Name ?? "null", + parameter.ParameterType.FullName!); return false; } } diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index 5ce8bc7d0..98209eeb7 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -25,9 +25,10 @@ internal interface IServiceScope : IAsyncDisposable /// Create an object. /// /// The type of object to create. + /// Defines which services are allowed to be directly resolved into this type. /// Scoped objects to be included in the constructor. /// The created object. - Task CreateAsync(Type objectType, params object[] scopedObjects); + Task CreateAsync(Type objectType, ObjectInstanceVisibility allowedVisibility, params object[] scopedObjects); /// /// Inject interfaces into public or static properties on the provided object. @@ -72,13 +73,13 @@ internal class ServiceScopeImpl : IServiceScope } /// - public Task CreateAsync(Type objectType, params object[] scopedObjects) + public Task CreateAsync(Type objectType, ObjectInstanceVisibility allowedVisibility, params object[] scopedObjects) { this.disposeLock.EnterReadLock(); try { ObjectDisposedException.ThrowIf(this.disposed, this); - return this.container.CreateAsync(objectType, scopedObjects, this); + return this.container.CreateAsync(objectType, allowedVisibility, scopedObjects, this); } finally { @@ -117,7 +118,9 @@ internal class ServiceScopeImpl : IServiceScope objectType, static (objectType, p) => p.Scope.container.CreateAsync( objectType, - p.Objects.Concat(p.Scope.privateScopedObjects).ToArray()), + ObjectInstanceVisibility.Internal, // We are allowed to resolve internal services here since this is a private scoped object. + p.Objects.Concat(p.Scope.privateScopedObjects).ToArray(), + p.Scope), (Scope: this, Objects: scopedObjects)); } finally diff --git a/Dalamud/Logging/Internal/TaskTracker.cs b/Dalamud/Logging/Internal/TaskTracker.cs index b45ed82d6..cb9a0db6d 100644 --- a/Dalamud/Logging/Internal/TaskTracker.cs +++ b/Dalamud/Logging/Internal/TaskTracker.cs @@ -200,22 +200,22 @@ internal class TaskTracker : IInternalDisposableService public StackTrace? StackTrace { get; set; } /// - /// Gets or sets a value indicating whether or not the task was completed. + /// Gets or sets a value indicating whether the task was completed. /// public bool IsCompleted { get; set; } /// - /// Gets or sets a value indicating whether or not the task faulted. + /// Gets or sets a value indicating whether the task faulted. /// public bool IsFaulted { get; set; } /// - /// Gets or sets a value indicating whether or not the task was canceled. + /// Gets or sets a value indicating whether the task was canceled. /// public bool IsCanceled { get; set; } /// - /// Gets or sets a value indicating whether or not the task was completed successfully. + /// Gets or sets a value indicating whether the task was completed successfully. /// public bool IsCompletedSuccessfully { get; set; } diff --git a/Dalamud/Logging/ScopedPluginLogService.cs b/Dalamud/Logging/ScopedPluginLogService.cs index 7305aa87b..5b0ca15e5 100644 --- a/Dalamud/Logging/ScopedPluginLogService.cs +++ b/Dalamud/Logging/ScopedPluginLogService.cs @@ -1,5 +1,6 @@ using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; @@ -20,6 +21,7 @@ namespace Dalamud.Logging; internal class ScopedPluginLogService : IServiceType, IPluginLog { private readonly LocalPlugin localPlugin; + private readonly PluginErrorHandler errorHandler; private readonly LoggingLevelSwitch levelSwitch; @@ -27,10 +29,12 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog /// Initializes a new instance of the class. /// /// The plugin that owns this service. - internal ScopedPluginLogService(LocalPlugin localPlugin) + /// Error notifier service. + internal ScopedPluginLogService(LocalPlugin localPlugin, PluginErrorHandler errorHandler) { this.localPlugin = localPlugin; - + this.errorHandler = errorHandler; + this.levelSwitch = new LoggingLevelSwitch(this.GetDefaultLevel()); var loggerConfiguration = new LoggerConfiguration() @@ -40,7 +44,7 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog this.Logger = loggerConfiguration.CreateLogger(); } - + /// public ILogger Logger { get; } @@ -50,7 +54,7 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog get => this.levelSwitch.MinimumLevel; set => this.levelSwitch.MinimumLevel = value; } - + /// public void Fatal(string messageTemplate, params object[] values) => this.Write(LogEventLevel.Fatal, null, messageTemplate, values); @@ -82,11 +86,11 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog /// public void Information(Exception? exception, string messageTemplate, params object[] values) => this.Write(LogEventLevel.Information, exception, messageTemplate, values); - + /// public void Info(string messageTemplate, params object[] values) => this.Information(messageTemplate, values); - + /// public void Info(Exception? exception, string messageTemplate, params object[] values) => this.Information(exception, messageTemplate, values); @@ -106,10 +110,13 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog /// public void Verbose(Exception? exception, string messageTemplate, params object[] values) => this.Write(LogEventLevel.Verbose, exception, messageTemplate, values); - + /// public void Write(LogEventLevel level, Exception? exception, string messageTemplate, params object[] values) { + if (level == LogEventLevel.Error) + this.errorHandler.NotifyError(); + this.Logger.Write( level, exception: exception, @@ -124,7 +131,7 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog private LogEventLevel GetDefaultLevel() { // TODO: Add some way to save log levels to a config. Or let plugins handle it? - + return this.localPlugin.IsDev ? LogEventLevel.Verbose : LogEventLevel.Debug; } } diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index d5cf360af..f82d241d4 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -19,6 +19,7 @@ using Dalamud.Interface; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings; +using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Plugin.Internal.Types; @@ -100,7 +101,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa public PluginLoadReason Reason { get; } /// - /// Gets a value indicating whether or not auto-updates have already completed this session. + /// Gets a value indicating whether auto-updates have already completed this session. /// public bool IsAutoUpdateComplete => Service.Get().IsAutoUpdateComplete; @@ -482,7 +483,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// public async Task CreateAsync(params object[] scopedObjects) where T : class => - (T)await this.plugin.ServiceScope!.CreateAsync(typeof(T), this.GetPublicIocScopes(scopedObjects)); + (T)await this.plugin.ServiceScope!.CreateAsync(typeof(T), ObjectInstanceVisibility.ExposedToPlugins, this.GetPublicIocScopes(scopedObjects)); /// public bool Inject(object instance, params object[] scopedObjects) diff --git a/Dalamud/Plugin/IDalamudPluginInterface.cs b/Dalamud/Plugin/IDalamudPluginInterface.cs index 100d4570e..5b7c3836e 100644 --- a/Dalamud/Plugin/IDalamudPluginInterface.cs +++ b/Dalamud/Plugin/IDalamudPluginInterface.cs @@ -35,7 +35,7 @@ public interface IDalamudPluginInterface /// What action caused this event to be fired. /// If this plugin was affected by the change. public delegate void ActivePluginsChangedDelegate(PluginListInvalidationKind kind, bool affectedThisPlugin); - + /// /// Event that gets fired when loc is changed /// @@ -52,7 +52,7 @@ public interface IDalamudPluginInterface PluginLoadReason Reason { get; } /// - /// Gets a value indicating whether or not auto-updates have already completed this session. + /// Gets a value indicating whether auto-updates have already completed this session. /// bool IsAutoUpdateComplete { get; } diff --git a/Dalamud/Plugin/InstalledPluginState.cs b/Dalamud/Plugin/InstalledPluginState.cs index 64c8e40a5..1c79e33f0 100644 --- a/Dalamud/Plugin/InstalledPluginState.cs +++ b/Dalamud/Plugin/InstalledPluginState.cs @@ -34,12 +34,12 @@ public interface IExposedPlugin bool IsTesting { get; } /// - /// Gets a value indicating whether or not this plugin is orphaned(belongs to a repo) or not. + /// Gets a value indicating whether this plugin is orphaned(belongs to a repo) or not. /// bool IsOrphaned { get; } /// - /// Gets a value indicating whether or not this plugin is serviced(repo still exists, but plugin no longer does). + /// Gets a value indicating whether this plugin is serviced(repo still exists, but plugin no longer does). /// bool IsDecommissioned { get; } diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index 413c9a171..a35928b8a 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -129,7 +129,7 @@ internal class AutoUpdateManager : IServiceType } /// - /// Gets a value indicating whether or not auto-updates have already completed this session. + /// Gets a value indicating whether auto-updates have already completed this session. /// public bool IsAutoUpdateComplete { get; private set; } @@ -458,7 +458,7 @@ internal class AutoUpdateManager : IServiceType .Where( p => !p.InstalledPlugin.IsDev && // Never update dev-plugins - p.InstalledPlugin.IsWantedByAnyProfile && // Never update plugins that are not wanted by any profile(not enabled) + (p.InstalledPlugin.IsWantedByAnyProfile || this.config.UpdateDisabledPlugins) && // Never update plugins that are not wanted by any profile(not enabled) !p.InstalledPlugin.Manifest.ScheduledForDeletion); // Never update plugins that we want to get rid of return updateablePlugins.Where(FilterPlugin).ToList(); @@ -499,7 +499,7 @@ internal class AutoUpdateManager : IServiceType condition.OnlyAny(ConditionFlag.NormalConditions, ConditionFlag.Jumping, ConditionFlag.Mounted, - ConditionFlag.UsingParasol); + ConditionFlag.UsingFashionAccessory); } private bool IsPluginManagerReady() diff --git a/Dalamud/Plugin/Internal/PluginErrorHandler.cs b/Dalamud/Plugin/Internal/PluginErrorHandler.cs new file mode 100644 index 000000000..f9d1f73f6 --- /dev/null +++ b/Dalamud/Plugin/Internal/PluginErrorHandler.cs @@ -0,0 +1,199 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; + +using ImGuiNET; + +using Serilog; + +namespace Dalamud.Plugin.Internal; + +/// +/// Service responsible for notifying the user when a plugin is creating errors. +/// +[ServiceManager.ScopedService] +internal class PluginErrorHandler : IServiceType +{ + private readonly LocalPlugin plugin; + private readonly NotificationManager notificationManager; + private readonly DalamudInterface di; + + private readonly Dictionary invokerCache = new(); + + private DateTime lastErrorTime = DateTime.MinValue; + private IActiveNotification? activeNotification; + + /// + /// Initializes a new instance of the class. + /// + /// The plugin we are notifying for. + /// The notification manager. + /// The dalamud interface class. + [ServiceManager.ServiceConstructor] + public PluginErrorHandler(LocalPlugin plugin, NotificationManager notificationManager, DalamudInterface di) + { + this.plugin = plugin; + this.notificationManager = notificationManager; + this.di = di; + } + + /// + /// Invoke the specified delegate and catch any exceptions that occur. + /// Writes an error message to the log if an exception occurs and shows + /// a notification if the plugin is a dev plugin and the user has enabled error notifications. + /// + /// The delegate to invoke. + /// A hint to show about the origin of the exception if an error occurs. + /// Arguments to the event handler. + /// The type of the delegate. + /// Whether invocation was successful/did not throw an exception. + public bool InvokeAndCatch( + TDelegate? eventHandler, + string hint, + params object[] args) + where TDelegate : Delegate + { + if (eventHandler == null) + return true; + + try + { + var invoker = this.GetInvoker(); + invoker(eventHandler, args); + return true; + } + catch (Exception ex) + { + Log.Error(ex, $"[{this.plugin.InternalName}] Exception in event handler {{EventHandlerName}}", hint); + this.NotifyError(); + return false; + } + } + + /// + /// Show a notification, if the plugin is a dev plugin and the user has enabled error notifications. + /// This function has a cooldown built-in. + /// + public void NotifyError() + { + if (this.plugin is not LocalDevPlugin devPlugin) + return; + + if (!devPlugin.NotifyForErrors) + return; + + // If the notification is already active, we don't need to show it again. + if (this.activeNotification is { DismissReason: null }) + return; + + var now = DateTime.UtcNow; + if (now - this.lastErrorTime < TimeSpan.FromMinutes(2)) + return; + + this.lastErrorTime = now; + + var creatingErrorsText = $"{devPlugin.Name} is creating errors"; + var notification = new Notification() + { + Title = creatingErrorsText, + Icon = INotificationIcon.From(FontAwesomeIcon.Bolt), + Type = NotificationType.Error, + InitialDuration = TimeSpan.FromSeconds(15), + MinimizedText = creatingErrorsText, + Content = $"The plugin '{devPlugin.Name}' is creating errors. Click 'Show console' to learn more.\n\n" + + $"You are seeing this because '{devPlugin.Name}' is a Dev Plugin.", + RespectUiHidden = false, + }; + + this.activeNotification = this.notificationManager.AddNotification(notification); + this.activeNotification.DrawActions += _ => + { + if (ImGui.Button("Show console")) + { + this.di.OpenLogWindow(this.plugin.InternalName); + this.activeNotification.DismissNow(); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Show the console filtered to this plugin"); + } + + ImGui.SameLine(); + + if (ImGui.Button("Disable notifications")) + { + devPlugin.NotifyForErrors = false; + this.activeNotification.DismissNow(); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Disable error notifications for this plugin"); + } + }; + } + + private static Action CreateInvoker() where TDelegate : Delegate + { + var delegateType = typeof(TDelegate); + var method = delegateType.GetMethod("Invoke"); + if (method == null) + throw new InvalidOperationException($"Delegate {delegateType} does not have an Invoke method."); + + var parameters = method.GetParameters(); + + // Create parameters for the lambda + var delegateParam = Expression.Parameter(delegateType, "d"); + var argsParam = Expression.Parameter(typeof(object[]), "args"); + + // Create expressions to convert array elements to parameter types + var callArgs = new Expression[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + { + var paramType = parameters[i].ParameterType; + var arrayAccess = Expression.ArrayIndex(argsParam, Expression.Constant(i)); + callArgs[i] = Expression.Convert(arrayAccess, paramType); + } + + // Create the delegate invocation expression + var callExpr = Expression.Call(delegateParam, method, callArgs); + + // If return type is not void, discard the result + Expression bodyExpr; + if (method.ReturnType != typeof(void)) + { + // Create a block that executes the call and then returns void + bodyExpr = Expression.Block( + Expression.Call(delegateParam, method, callArgs), + Expression.Empty()); + } + else + { + bodyExpr = callExpr; + } + + // Compile and return the lambda + var lambda = Expression.Lambda>( + bodyExpr, delegateParam, argsParam); + return lambda.Compile(); + } + + private Action GetInvoker() where TDelegate : Delegate + { + var delegateType = typeof(TDelegate); + + if (!this.invokerCache.TryGetValue(delegateType, out var cachedInvoker)) + { + cachedInvoker = CreateInvoker(); + this.invokerCache[delegateType] = cachedInvoker; + } + + return (Action)cachedInvoker; + } +} diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index a20f87241..bfb1b3430 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -284,7 +284,7 @@ internal class PluginManager : IInternalDisposableService /// Check if a manifest even has an available testing version. /// /// The manifest to test. - /// Whether or not a testing version is available. + /// Whether a testing version is available. public static bool HasTestingVersion(IPluginManifest manifest) { var av = manifest.AssemblyVersion; @@ -663,6 +663,8 @@ internal class PluginManager : IInternalDisposableService _ = Task.Run( async () => { + Log.Verbose("Starting async boot load"); + // Load plugins that want to be loaded during Framework.Tick var framework = await Service.GetAsync().ConfigureAwait(false); await framework.RunOnTick( @@ -671,27 +673,35 @@ internal class PluginManager : IInternalDisposableService syncPlugins.Where(def => def.Manifest?.LoadRequiredState == 1), tokenSource.Token), cancellationToken: tokenSource.Token).ConfigureAwait(false); + Log.Verbose("Loaded FrameworkTickSync plugins (LoadRequiredState == 1)"); + loadTasks.Add(LoadPluginsAsync( "FrameworkTickAsync", asyncPlugins.Where(def => def.Manifest?.LoadRequiredState == 1), tokenSource.Token)); + Log.Verbose("Kicked off FrameworkTickAsync plugins (LoadRequiredState == 1)"); // Load plugins that want to be loaded during Framework.Tick, when drawing facilities are available _ = await Service.GetAsync().ConfigureAwait(false); + Log.Verbose(" InterfaceManager is ready, starting to load DrawAvailableSync plugins"); await framework.RunOnTick( () => LoadPluginsSync( "DrawAvailableSync", syncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null), tokenSource.Token), cancellationToken: tokenSource.Token); + Log.Verbose("Loaded DrawAvailableSync plugins (LoadRequiredState == 0 or null)"); + loadTasks.Add(LoadPluginsAsync( "DrawAvailableAsync", asyncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null), tokenSource.Token)); + Log.Verbose("Kicked off DrawAvailableAsync plugins (LoadRequiredState == 0 or null)"); // Save signatures when all plugins are done loading, successful or not. try { + Log.Verbose("Now waiting for {NumTasks} async load tasks", loadTasks.Count); await Task.WhenAll(loadTasks).ConfigureAwait(false); Log.Information("Loaded plugins on boot"); } @@ -715,8 +725,13 @@ internal class PluginManager : IInternalDisposableService } this.StartupLoadTracking = null; - }, - tokenSource.Token); + }, tokenSource.Token).ContinueWith(t => + { + if (t.IsFaulted) + { + Log.Error(t.Exception, "Failed to load FrameworkTickAsync/DrawAvailableAsync plugins"); + } + }, TaskContinuationOptions.OnlyOnFaulted); } /// @@ -776,7 +791,8 @@ internal class PluginManager : IInternalDisposableService /// only shown as disabled in the installed plugins window. This is a modified version of LoadAllPlugins that works /// a little differently. /// - public void ScanDevPlugins() + /// A representing the asynchronous operation. This function generally will not block as new plugins aren't loaded. + public async Task ScanDevPluginsAsync() { // devPlugins are more freeform. Look for any dll and hope to get lucky. var devDllFiles = new List(); @@ -823,8 +839,7 @@ internal class PluginManager : IInternalDisposableService try { // Add them to the list and let the user decide, nothing is auto-loaded. - this.LoadPluginAsync(dllFile, manifest, PluginLoadReason.Installer, isDev: true, doNotLoad: true) - .Wait(); + await this.LoadPluginAsync(dllFile, manifest, PluginLoadReason.Installer, isDev: true, doNotLoad: true); listChanged = true; } catch (InvalidPluginException) @@ -1037,7 +1052,7 @@ internal class PluginManager : IInternalDisposableService /// /// The available plugin update. /// Whether to notify that installed plugins have changed afterwards. - /// Whether or not to actually perform the update, or just indicate success. + /// Whether to actually perform the update, or just indicate success. /// The status of the update. public async Task UpdateSinglePluginAsync(AvailablePluginUpdate metadata, bool notify, bool dryRun) { @@ -1188,32 +1203,20 @@ internal class PluginManager : IInternalDisposableService { // Testing exclusive if (manifest.IsTestingExclusive && !this.configuration.DoPluginTest) - { - Log.Verbose($"Testing exclusivity: {manifest.InternalName} - {manifest.AssemblyVersion} - {manifest.TestingAssemblyVersion}"); return false; - } // Applicable version if (manifest.ApplicableVersion < this.dalamud.StartInfo.GameVersion) - { - Log.Verbose($"Game version: {manifest.InternalName} - {manifest.AssemblyVersion} - {manifest.TestingAssemblyVersion}"); return false; - } // API level - we keep the API before this in the installer to show as "outdated" var effectiveApiLevel = this.UseTesting(manifest) && manifest.TestingDalamudApiLevel != null ? manifest.TestingDalamudApiLevel.Value : manifest.DalamudApiLevel; if (effectiveApiLevel < DalamudApiLevel - 1 && !this.LoadAllApiLevels) - { - Log.Verbose($"API Level: {manifest.InternalName} - {manifest.AssemblyVersion} - {manifest.TestingAssemblyVersion}"); return false; - } // Banned if (this.IsManifestBanned(manifest)) - { - Log.Verbose($"Banned: {manifest.InternalName} - {manifest.AssemblyVersion} - {manifest.TestingAssemblyVersion}"); return false; - } return true; } @@ -1572,6 +1575,8 @@ internal class PluginManager : IInternalDisposableService /// The loaded plugin. private async Task LoadPluginAsync(FileInfo dllFile, LocalPluginManifest manifest, PluginLoadReason reason, bool isDev = false, bool isBoot = false, bool doNotLoad = false) { + // TODO: Split this function - it should only take care of adding the plugin to the list, not loading itself, that should be done through the plugin instance + var loadPlugin = !doNotLoad; LocalPlugin? plugin; @@ -1582,21 +1587,34 @@ internal class PluginManager : IInternalDisposableService throw new Exception("No internal name"); } - if (isDev) + // Track the plugin as soon as it is instantiated to prevent it from being loaded twice, + // if the installer or DevPlugin scanner is attempting to add plugins while we are still loading boot plugins + lock (this.pluginListLock) { - Log.Information("Loading dev plugin {Name}", manifest.InternalName); - plugin = new LocalDevPlugin(dllFile, manifest); + // Check if this plugin is already loaded + if (this.installedPluginsList.Any(lp => lp.DllFile.FullName == dllFile.FullName)) + throw new InvalidOperationException("Plugin at the provided path is already loaded"); - // This is a dev plugin - turn ImGui asserts on by default if we haven't chosen yet - // TODO(goat): Re-enable this when we have better tracing for what was rendering when - // this.configuration.ImGuiAssertsEnabledAtStartup ??= true; - } - else - { - Log.Information("Loading plugin {Name}", manifest.InternalName); - plugin = new LocalPlugin(dllFile, manifest); + if (isDev) + { + Log.Information("Loading dev plugin {Name}", manifest.InternalName); + plugin = new LocalDevPlugin(dllFile, manifest); + + // This is a dev plugin - turn ImGui asserts on by default if we haven't chosen yet + // TODO(goat): Re-enable this when we have better tracing for what was rendering when + // this.configuration.ImGuiAssertsEnabledAtStartup ??= true; + } + else + { + Log.Information("Loading plugin {Name}", manifest.InternalName); + plugin = new LocalPlugin(dllFile, manifest); + } + + this.installedPluginsList.Add(plugin); } + Log.Verbose("Starting to load plugin {Name} at {FileLocation}", manifest.InternalName, dllFile.FullName); + // Perform a migration from InternalName to GUIDs. The plugin should definitely have a GUID here. // This will also happen if you are installing a plugin with the installer, and that's intended! // It means that, if you have a profile which has unsatisfied plugins, installing a matching plugin will @@ -1697,43 +1715,34 @@ internal class PluginManager : IInternalDisposableService catch (BannedPluginException) { // Out of date plugins get added so they can be updated. - Log.Information($"Plugin was banned, adding anyways: {dllFile.Name}"); + Log.Information("{InternalName}: Plugin was banned, adding anyways", plugin.Manifest.InternalName); } catch (Exception ex) { if (plugin.IsDev) { // Dev plugins always get added to the list so they can be fiddled with in the UI - Log.Information(ex, $"Dev plugin failed to load, adding anyways: {dllFile.Name}"); - - // NOTE(goat): This can't work - plugins don't "unload" if they fail to load. - // plugin.Disable(); // Disable here, otherwise you can't enable+load later + Log.Information(ex, "{InternalName}: Dev plugin failed to load", plugin.Manifest.InternalName); } else if (plugin.IsOutdated) { // Out of date plugins get added, so they can be updated. - Log.Information(ex, $"Plugin was outdated, adding anyways: {dllFile.Name}"); + Log.Information(ex, "{InternalName}: Plugin was outdated", plugin.Manifest.InternalName); } else if (plugin.IsOrphaned) { // Orphaned plugins get added, so that users aren't confused. - Log.Information(ex, $"Plugin was orphaned, adding anyways: {dllFile.Name}"); + Log.Information(ex, "{InternalName}: Plugin was orphaned", plugin.Manifest.InternalName); } else if (isBoot) { // During boot load, plugins always get added to the list so they can be fiddled with in the UI - Log.Information(ex, $"Regular plugin failed to load, adding anyways: {dllFile.Name}"); - - // NOTE(goat): This can't work - plugins don't "unload" if they fail to load. - // plugin.Disable(); // Disable here, otherwise you can't enable+load later + Log.Information(ex, "{InternalName}: Regular plugin failed to load", plugin.Manifest.InternalName); } else if (!plugin.CheckPolicy()) { // During boot load, plugins always get added to the list so they can be fiddled with in the UI - Log.Information(ex, $"Plugin not loaded due to policy, adding anyways: {dllFile.Name}"); - - // NOTE(goat): This can't work - plugins don't "unload" if they fail to load. - // plugin.Disable(); // Disable here, otherwise you can't enable+load later + Log.Information(ex, "{InternalName}: Plugin not loaded due to policy", plugin.Manifest.InternalName); } else { @@ -1742,14 +1751,6 @@ internal class PluginManager : IInternalDisposableService } } - if (plugin == null) - throw new Exception("Plugin was null when adding to list"); - - lock (this.pluginListLock) - { - this.installedPluginsList.Add(plugin); - } - // Mark as finished loading if (manifest.LoadSync) this.StartupLoadTracking?.Finish(manifest.InternalName); @@ -1774,6 +1775,7 @@ internal class PluginManager : IInternalDisposableService 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.MinimumDalamudVersion == null || Util.AssemblyVersionParsed >= remoteManifest.MinimumDalamudVersion) .Where(remoteManifest => { var useTesting = this.UseTesting(remoteManifest); @@ -1844,18 +1846,27 @@ internal class PluginManager : IInternalDisposableService _ = this.SetPluginReposFromConfigAsync(false); this.OnInstalledPluginsChanged += () => Task.Run(Troubleshooting.LogTroubleshooting); - Log.Information("[T3] PM repos OK!"); + Log.Information("Repos loaded!"); } using (Timings.Start("PM Cleanup Plugins")) { this.CleanupPlugins(); - Log.Information("[T3] PMC OK!"); + Log.Information("Plugin cleanup OK!"); } using (Timings.Start("PM Load Sync Plugins")) { - var loadAllPlugins = Task.Run(this.LoadAllPlugins); + var loadAllPlugins = Task.Run(this.LoadAllPlugins) + .ContinueWith(t => + { + if (t.IsFaulted) + { + Log.Error(t.Exception, "Error in LoadAllPlugins()"); + } + + _ = Task.Run(Troubleshooting.LogTroubleshooting); + }); // We wait for all blocking services and tasks to finish before kicking off the main thread in any mode. // This means that we don't want to block here if this stupid thing isn't enabled. @@ -1865,10 +1876,8 @@ internal class PluginManager : IInternalDisposableService loadAllPlugins.Wait(); } - Log.Information("[T3] PML OK!"); + Log.Information("Boot load started"); } - - _ = Task.Run(Troubleshooting.LogTroubleshooting); } catch (Exception ex) { diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 2c254167e..d899b0cca 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -24,8 +24,8 @@ internal class Profile /// /// The manager this profile belongs to. /// The model this profile is tied to. - /// Whether or not this profile is the default profile. - /// Whether or not this profile was initialized during bootup. + /// Whether this profile is the default profile. + /// Whether this profile was initialized during bootup. public Profile(ProfileManager manager, ProfileModel model, bool isDefaultProfile, bool isBoot) { this.manager = manager; @@ -33,6 +33,18 @@ internal class Profile this.modelV1 = model as ProfileModelV1 ?? throw new ArgumentException("Model was null or unhandled version"); + // Migrate "policy" + if (this.modelV1.StartupPolicy == null) + { +#pragma warning disable CS0618 + this.modelV1.StartupPolicy = this.modelV1.AlwaysEnableOnBoot + ? ProfileModelV1.ProfileStartupPolicy.AlwaysEnable + : ProfileModelV1.ProfileStartupPolicy.RememberState; +#pragma warning restore CS0618 + + Service.Get().QueueSave(); + } + // We don't actually enable plugins here, PM will do it on bootup if (isDefaultProfile) { @@ -40,20 +52,40 @@ internal class Profile this.IsEnabled = this.modelV1.IsEnabled = true; this.Name = this.modelV1.Name = "DEFAULT"; } - else if (this.modelV1.AlwaysEnableOnBoot && isBoot) + else if (isBoot) { - this.IsEnabled = true; - Log.Verbose("{Guid} set enabled because bootup", this.modelV1.Guid); - } - else if (this.modelV1.IsEnabled) - { - this.IsEnabled = true; - Log.Verbose("{Guid} set enabled because remember", this.modelV1.Guid); + if (this.modelV1.StartupPolicy == ProfileModelV1.ProfileStartupPolicy.AlwaysEnable) + { + this.IsEnabled = true; + Log.Verbose("{Guid} set enabled because always enable", this.modelV1.Guid); + } + else if (this.modelV1.StartupPolicy == ProfileModelV1.ProfileStartupPolicy.AlwaysDisable) + { + this.IsEnabled = false; + Log.Verbose("{Guid} set disabled because always disable", this.modelV1.Guid); + } + else if (this.modelV1.StartupPolicy == ProfileModelV1.ProfileStartupPolicy.RememberState) + { + this.IsEnabled = this.modelV1.IsEnabled; + Log.Verbose("{Guid} set enabled because remember", this.modelV1.Guid); + } + else + { + throw new ArgumentOutOfRangeException(nameof(this.modelV1.StartupPolicy)); + } } else { Log.Verbose("{Guid} not enabled", this.modelV1.Guid); } + + Log.Verbose("Init profile {Guid} ({Name}) enabled:{Enabled} policy:{Policy} plugins:{NumPlugins} will be enabled:{Status}", + this.modelV1.Guid, + this.modelV1.Name, + this.modelV1.IsEnabled, + this.modelV1.StartupPolicy, + this.modelV1.Plugins.Count, + this.IsEnabled); } /// @@ -72,12 +104,12 @@ internal class Profile /// /// Gets or sets a value indicating whether this profile shall always be enabled at boot. /// - public bool AlwaysEnableAtBoot + public ProfileModelV1.ProfileStartupPolicy StartupPolicy { - get => this.modelV1.AlwaysEnableOnBoot; + get => this.modelV1.StartupPolicy ?? ProfileModelV1.ProfileStartupPolicy.RememberState; set { - this.modelV1.AlwaysEnableOnBoot = value; + this.modelV1.StartupPolicy = value; Service.Get().QueueSave(); } } @@ -88,12 +120,12 @@ internal class Profile public Guid Guid => this.modelV1.Guid; /// - /// Gets a value indicating whether or not this profile is currently enabled. + /// Gets a value indicating whether this profile is currently enabled. /// public bool IsEnabled { get; private set; } /// - /// Gets a value indicating whether or not this profile is the default profile. + /// Gets a value indicating whether this profile is the default profile. /// public bool IsDefaultProfile { get; } @@ -119,8 +151,8 @@ internal class Profile /// Set this profile's state. This cannot be called for the default profile. /// This will block until all states have been applied. /// - /// Whether or not the profile is enabled. - /// Whether or not the current state should immediately be applied. + /// Whether the profile is enabled. + /// Whether the current state should immediately be applied. /// Thrown when an untoggleable profile is toggled. /// A representing the asynchronous operation. public async Task SetStateAsync(bool enabled, bool apply = true) @@ -158,13 +190,13 @@ internal class Profile /// /// The ID of the plugin. /// The internal name of the plugin, if available. - /// Whether or not the plugin should be enabled. - /// Whether or not the current state should immediately be applied. + /// Whether the plugin should be enabled. + /// Whether the current state should immediately be applied. /// A representing the asynchronous operation. public async Task AddOrUpdateAsync(Guid workingPluginId, string? internalName, bool state, bool apply = true) { Debug.Assert(workingPluginId != Guid.Empty, "Trying to add plugin with empty guid"); - + lock (this) { var existing = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId); @@ -182,9 +214,9 @@ internal class Profile }); } } - + Log.Information("Adding plugin {Plugin}({Guid}) to profile {Profile} with state {State}", internalName, workingPluginId, this.Guid, state); - + // We need to remove this plugin from the default profile, if it declares it. if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(workingPluginId) != null) { @@ -203,9 +235,9 @@ internal class Profile /// This will block until all states have been applied. /// /// The ID of the plugin. - /// Whether or not the current state should immediately be applied. + /// Whether the current state should immediately be applied. /// - /// Whether or not to throw when a plugin is removed from the default profile, without being in another profile. + /// Whether to throw when a plugin is removed from the default profile, without being in another profile. /// Used to prevent orphan plugins, but can be ignored when cleaning up old entries. /// /// A representing the asynchronous operation. @@ -221,7 +253,7 @@ internal class Profile if (!this.modelV1.Plugins.Remove(entry)) throw new Exception("Couldn't remove plugin from model collection"); } - + Log.Information("Removing plugin {Plugin}({Guid}) from profile {Profile}", entry.InternalName, entry.WorkingPluginId, this.Guid); // We need to add this plugin back to the default profile, if we were the last profile to have it. @@ -260,7 +292,7 @@ internal class Profile // TODO: What should happen if a profile has a GUID locked in, but the plugin // is not installed anymore? That probably means that the user uninstalled the plugin // and is now reinstalling it. We should still satisfy that and update the ID. - + if (plugin.InternalName == internalName && plugin.WorkingPluginId == Guid.Empty) { plugin.WorkingPluginId = newGuid; @@ -268,7 +300,7 @@ internal class Profile } } } - + Service.Get().QueueSave(); } @@ -319,7 +351,7 @@ internal sealed class PluginNotFoundException : ProfileOperationException : base($"The plugin '{internalName}' was not found in the profile") { } - + /// /// Initializes a new instance of the class. /// diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 8a1711b0d..775ff7a72 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -54,7 +54,7 @@ internal class ProfileManager : IServiceType public IEnumerable Profiles => this.profiles; /// - /// Gets a value indicating whether or not the profile manager is busy enabling/disabling plugins. + /// Gets a value indicating whether the profile manager is busy enabling/disabling plugins. /// public bool IsBusy => this.isBusy; @@ -71,8 +71,8 @@ internal class ProfileManager : IServiceType /// The ID of the plugin. /// The internal name of the plugin, if available. /// 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. + /// Whether the plugin should be added to the default preset, if it's not present in any preset. + /// Whether the plugin shall be enabled. public async Task GetWantStateAsync(Guid workingPluginId, string? internalName, bool defaultState, bool addIfNotDeclared = true) { var want = false; @@ -106,7 +106,7 @@ internal class ProfileManager : IServiceType /// Check whether a plugin is declared in any profile. /// /// The ID of the plugin. - /// Whether or not the plugin is in any profile. + /// Whether the plugin is in any profile. public bool IsInAnyProfile(Guid workingPluginId) { lock (this.profiles) @@ -118,7 +118,7 @@ internal class ProfileManager : IServiceType /// A plugin can never be in the default profile if it is in any other profile. /// /// The ID of the plugin. - /// Whether or not the plugin is in the default profile. + /// Whether the plugin is in the default profile. public bool IsInDefaultProfile(Guid workingPluginId) => this.DefaultProfile.WantsPlugin(workingPluginId) != null; @@ -193,6 +193,10 @@ internal class ProfileManager : IServiceType } } } + else + { + throw new InvalidOperationException("Unsupported profile model version"); + } this.config.SavedProfiles!.Add(newModel); this.config.QueueSave(); diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs index 99da4263b..a1a327c1d 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs @@ -9,19 +9,47 @@ namespace Dalamud.Plugin.Internal.Profiles; /// public class ProfileModelV1 : ProfileModel { + /// + /// Enum representing the startup policy of a profile. + /// + public enum ProfileStartupPolicy + { + /// + /// Remember the last state of the profile. + /// + RememberState, + + /// + /// Always enable the profile. + /// + AlwaysEnable, + + /// + /// Always disable the profile. + /// + AlwaysDisable, + } + /// /// Gets the prefix of this version. /// public static string SerializedPrefix => "DP1"; /// - /// Gets or sets a value indicating whether or not this profile should always be enabled at boot. + /// Gets or sets a value indicating whether this profile should always be enabled at boot. /// [JsonProperty("b")] + [Obsolete("Superseded by StartupPolicy")] public bool AlwaysEnableOnBoot { get; set; } = false; /// - /// Gets or sets a value indicating whether or not this profile is currently enabled. + /// Gets or sets the policy to use when Dalamud is loading. + /// + [JsonProperty("p")] + public ProfileStartupPolicy? StartupPolicy { get; set; } + + /// + /// Gets or sets a value indicating whether this profile is currently enabled. /// [JsonProperty("e")] public bool IsEnabled { get; set; } = false; @@ -46,14 +74,14 @@ public class ProfileModelV1 : ProfileModel /// Gets or sets the internal name of the plugin. /// public string? InternalName { get; set; } - + /// /// Gets or sets an ID uniquely identifying this specific instance of a plugin. /// public Guid WorkingPluginId { get; set; } /// - /// Gets or sets a value indicating whether or not this entry is enabled. + /// Gets or sets a value indicating whether this entry is enabled. /// public bool IsEnabled { get; set; } } diff --git a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs index 7909981bc..a2805ff6e 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs @@ -10,7 +10,7 @@ internal class ProfilePluginEntry /// /// The internal name of the plugin. /// The ID of the plugin. - /// A value indicating whether or not this entry is enabled. + /// A value indicating whether this entry is enabled. public ProfilePluginEntry(string internalName, Guid workingPluginId, bool state) { this.InternalName = internalName; @@ -22,14 +22,14 @@ internal class ProfilePluginEntry /// Gets the internal name of the plugin. /// public string InternalName { get; } - + /// /// Gets or sets an ID uniquely identifying this specific instance of a plugin. /// public Guid WorkingPluginId { get; set; } /// - /// Gets a value indicating whether or not this entry is enabled. + /// Gets a value indicating whether this entry is enabled. /// public bool IsEnabled { get; } } diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs index 581bfd724..34b54163a 100644 --- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs @@ -15,7 +15,7 @@ namespace Dalamud.Plugin.Internal.Types; /// This class represents a dev plugin and all facets of its lifecycle. /// The DLL on disk, dependencies, loaded assembly, etc. /// -internal class LocalDevPlugin : LocalPlugin +internal sealed class LocalDevPlugin : LocalPlugin { private static readonly ModuleLog Log = new("PLUGIN"); @@ -41,7 +41,7 @@ internal class LocalDevPlugin : LocalPlugin configuration.DevPluginSettings[dllFile.FullName] = this.devSettings = new DevPluginSettings(); configuration.QueueSave(); } - + // Legacy dev plugins might not have this! if (this.devSettings.WorkingPluginId == Guid.Empty) { @@ -85,7 +85,17 @@ internal class LocalDevPlugin : LocalPlugin } } } - + + /// + /// Gets or sets a value indicating whether users should be notified when this plugin + /// is causing errors. + /// + public bool NotifyForErrors + { + get => this.devSettings.NotifyForErrors; + set => this.devSettings.NotifyForErrors = value; + } + /// /// Gets an ID uniquely identifying this specific instance of a devPlugin. /// @@ -152,7 +162,7 @@ internal class LocalDevPlugin : LocalPlugin if (manifestPath.Exists) this.manifest = LocalPluginManifest.Load(manifestPath) ?? throw new Exception("Could not reload manifest."); } - + /// protected override void OnPreReload() { diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 1b9025538..4b2b62669 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -15,6 +15,7 @@ using Dalamud.Plugin.Internal.Exceptions; using Dalamud.Plugin.Internal.Loader; using Dalamud.Plugin.Internal.Profiles; using Dalamud.Plugin.Internal.Types.Manifest; +using Dalamud.Utility; namespace Dalamud.Plugin.Internal.Types; @@ -62,12 +63,13 @@ internal class LocalPlugin : IAsyncDisposable } this.DllFile = dllFile; - this.State = PluginState.Unloaded; // Although it is conditionally used here, we need to set the initial value regardless. this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile); this.manifest = manifest; + this.State = PluginState.Unloaded; + var needsSaveDueToLegacyFiles = false; // This converts from the ".disabled" file feature to the manifest instead. @@ -177,13 +179,13 @@ internal class LocalPlugin : IAsyncDisposable public bool IsTesting => this.manifest.IsTestingExclusive || this.manifest.Testing; /// - /// Gets a value indicating whether or not this plugin is orphaned(belongs to a repo) or not. + /// Gets a value indicating whether this plugin is orphaned(belongs to a repo) or not. /// public bool IsOrphaned => !this.IsDev && this.GetSourceRepository() == null; /// - /// Gets a value indicating whether or not this plugin is serviced(repo still exists, but plugin no longer does). + /// Gets a value indicating whether this plugin is serviced(repo still exists, but plugin no longer does). /// public bool IsDecommissioned => !this.IsDev && this.GetSourceRepository()?.State == PluginRepositoryState.Success && @@ -312,6 +314,9 @@ internal class LocalPlugin : IAsyncDisposable if (!this.CheckPolicy()) throw new PluginPreconditionFailedException($"Unable to load {this.Name} as a load policy forbids it"); + if (this.Manifest.MinimumDalamudVersion != null && this.Manifest.MinimumDalamudVersion > Util.AssemblyVersionParsed) + throw new PluginPreconditionFailedException($"Unable to load {this.Name}, Dalamud version is lower than minimum required version {this.Manifest.MinimumDalamudVersion}"); + this.State = PluginState.Loading; Log.Information($"Loading {this.DllFile.Name}"); @@ -352,19 +357,13 @@ internal class LocalPlugin : IAsyncDisposable } this.loader.Reload(); + this.RefreshAssemblyInformation(); } - // Load the assembly - this.pluginAssembly ??= this.loader.LoadDefaultAssembly(); - - this.AssemblyName = this.pluginAssembly.GetName(); - - // Find the plugin interface implementation. It is guaranteed to exist after checking in the ctor. - this.pluginType ??= this.pluginAssembly.GetTypes() - .First(type => type.IsAssignableTo(typeof(IDalamudPlugin))); + Log.Verbose("{Name} ({Guid}): Have type", this.InternalName, this.EffectiveWorkingPluginId); // Check for any loaded plugins with the same assembly name - var assemblyName = this.pluginAssembly.GetName().Name; + var assemblyName = this.pluginAssembly!.GetName().Name; foreach (var otherPlugin in pluginManager.InstalledPlugins) { // During hot-reloading, this plugin will be in the plugin list, and the instance will have been disposed @@ -376,7 +375,7 @@ internal class LocalPlugin : IAsyncDisposable if (otherPluginAssemblyName == assemblyName && otherPluginAssemblyName != null) { this.State = PluginState.Unloaded; - Log.Debug($"Duplicate assembly: {this.Name}"); + Log.Debug("Duplicate assembly: {Name}", this.InternalName); throw new DuplicatePluginException(assemblyName); } @@ -392,7 +391,7 @@ internal class LocalPlugin : IAsyncDisposable this.instance = await CreatePluginInstance( this.manifest, this.serviceScope, - this.pluginType, + this.pluginType!, this.dalamudInterface); this.State = PluginState.Loaded; Log.Information("Finished loading {PluginName}", this.InternalName); @@ -504,7 +503,7 @@ internal class LocalPlugin : IAsyncDisposable /// /// Check if anything forbids this plugin from loading. /// - /// Whether or not this plugin shouldn't load. + /// Whether this plugin shouldn't load. public bool CheckPolicy() { var startInfo = Service.Get().StartInfo; @@ -578,7 +577,7 @@ internal class LocalPlugin : IAsyncDisposable var newInstanceTask = forceFrameworkThread ? framework.RunOnFrameworkThread(Create) : Create(); return await newInstanceTask.ConfigureAwait(false); - async Task Create() => (IDalamudPlugin)await scope.CreateAsync(type, dalamudInterface); + async Task Create() => (IDalamudPlugin)await scope.CreateAsync(type, ObjectInstanceVisibility.ExposedToPlugins, dalamudInterface); } private static void SetupLoaderConfig(LoaderConfig config) @@ -620,42 +619,60 @@ internal class LocalPlugin : IAsyncDisposable throw; } + this.RefreshAssemblyInformation(); + } + + private void RefreshAssemblyInformation() + { + if (this.loader == null) + throw new InvalidOperationException("No loader available"); + try { this.pluginAssembly = this.loader.LoadDefaultAssembly(); + this.AssemblyName = this.pluginAssembly.GetName(); } catch (Exception ex) { - this.pluginAssembly = null; - this.pluginType = null; - this.loader.Dispose(); - + this.ResetLoader(); Log.Error(ex, $"Not a plugin: {this.DllFile.FullName}"); throw new InvalidPluginException(this.DllFile); } + if (this.pluginAssembly == null) + { + this.ResetLoader(); + Log.Error("Plugin assembly is null: {DllFileFullName}", this.DllFile.FullName); + throw new InvalidPluginException(this.DllFile); + } + try { this.pluginType = this.pluginAssembly.GetTypes().FirstOrDefault(type => type.IsAssignableTo(typeof(IDalamudPlugin))); } catch (ReflectionTypeLoadException ex) { - Log.Error(ex, $"Could not load one or more types when searching for IDalamudPlugin: {this.DllFile.FullName}"); - // Something blew up when parsing types, but we still want to look for IDalamudPlugin. Let Load() handle the error. - this.pluginType = ex.Types.FirstOrDefault(type => type != null && type.IsAssignableTo(typeof(IDalamudPlugin))); + this.ResetLoader(); + Log.Error(ex, "Could not load one or more types when searching for IDalamudPlugin: {DllFileFullName}", this.DllFile.FullName); + throw; } - if (this.pluginType == default) + if (this.pluginType == null) { - this.pluginAssembly = null; - this.pluginType = null; - this.loader.Dispose(); - - Log.Error($"Nothing inherits from IDalamudPlugin: {this.DllFile.FullName}"); + this.ResetLoader(); + Log.Error("Nothing inherits from IDalamudPlugin: {DllFileFullName}", this.DllFile.FullName); throw new InvalidPluginException(this.DllFile); } } + private void ResetLoader() + { + this.pluginAssembly = null; + this.pluginType = null; + this.loader?.Dispose(); + this.loader = null; + } + /// Clears and disposes all resources associated with the plugin instance. /// Whether to clear and dispose . /// Exceptions, if any occurred. diff --git a/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs index 4b0951397..5ab5abbc1 100644 --- a/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs @@ -16,7 +16,7 @@ public interface IPluginManifest /// Gets the public name of the plugin. /// public string Name { get; } - + /// /// Gets a punchline of the plugins functions. /// @@ -26,7 +26,7 @@ public interface IPluginManifest /// Gets the author/s of the plugin. /// public string Author { get; } - + /// /// Gets a value indicating whether the plugin can be unloaded asynchronously. /// @@ -41,17 +41,22 @@ public interface IPluginManifest /// Gets the assembly version of the plugin's testing variant. /// public Version? TestingAssemblyVersion { get; } - + + /// + /// Gets the minimum Dalamud assembly version this plugin requires. + /// + public Version? MinimumDalamudVersion { get; } + /// /// Gets the DIP17 channel name. /// public string? Dip17Channel { get; } - + /// /// Gets the last time this plugin was updated. /// public long LastUpdate { get; } - + /// /// Gets a changelog, null if none exists. /// @@ -88,7 +93,7 @@ public interface IPluginManifest /// Gets an URL to the website or source code of the plugin. /// public string? RepoUrl { get; } - + /// /// Gets a description of the plugins functions. /// diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs index 01951c8a6..57001d63b 100644 --- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs @@ -42,7 +42,7 @@ internal record PluginManifest : IPluginManifest public List? CategoryTags { get; init; } /// - /// Gets or sets a value indicating whether or not the plugin is hidden in the plugin installer. + /// Gets or sets a value indicating whether the plugin is hidden in the plugin installer. /// This value comes from the plugin master and is in addition to the list of hidden names kept by Dalamud. /// [JsonProperty] @@ -75,6 +75,10 @@ internal record PluginManifest : IPluginManifest [JsonConverter(typeof(GameVersionConverter))] public GameVersion? ApplicableVersion { get; init; } = GameVersion.Any; + /// + [JsonProperty] + public Version? MinimumDalamudVersion { get; init; } + /// [JsonProperty] public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel; diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs index c6ec5a941..e534eafb4 100644 --- a/Dalamud/Plugin/Services/IAddonEventManager.cs +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -13,8 +13,16 @@ public interface IAddonEventManager /// Event type for this event handler. /// The parent addon for this event handler. /// The specific node that will trigger this event handler. + [Obsolete("Use AddonEventDelegate instead")] public delegate void AddonEventHandler(AddonEventType atkEventType, nint atkUnitBase, nint atkResNode); + /// + /// Delegate to be called when an event is received. + /// + /// The AtkEventType that triggered this event. + /// The event data object for use in handling this event. + public delegate void AddonEventDelegate(AddonEventType atkEventType, AddonEventData data); + /// /// Registers an event handler for the specified addon, node, and type. /// @@ -23,8 +31,19 @@ public interface IAddonEventManager /// The event type for this event. /// The handler to call when event is triggered. /// IAddonEventHandle used to remove the event. Null if no event was added. + [Obsolete("Use AddEvent with AddonEventDelegate instead of AddonEventHandler")] IAddonEventHandle? AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType eventType, AddonEventHandler eventHandler); + /// + /// Registers an event handler for the specified addon, node, and type. + /// + /// The parent addon for this event. + /// The node that will trigger this event. + /// The event type for this event. + /// The handler to call when event is triggered. + /// IAddonEventHandle used to remove the event. Null if no event was added. + IAddonEventHandle? AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType eventType, AddonEventDelegate eventDelegate); + /// /// Unregisters an event handler with the specified event id and event type. /// diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index bac2b3e3f..60d8a17e2 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -79,7 +79,7 @@ public interface IClientState /// Gets the current Territory the player resides in. /// public ushort TerritoryType { get; } - + /// /// Gets the current Map the player resides in. /// @@ -101,17 +101,17 @@ public interface IClientState public bool IsLoggedIn { get; } /// - /// Gets a value indicating whether or not the user is playing PvP. + /// Gets a value indicating whether the user is playing PvP. /// public bool IsPvP { get; } /// - /// Gets a value indicating whether or not the user is playing PvP, excluding the Wolves' Den. + /// Gets a value indicating whether the user is playing PvP, excluding the Wolves' Den. /// public bool IsPvPExcludingDen { get; } - + /// - /// Gets a value indicating whether the client is currently in Group Pose (GPose) mode. + /// Gets a value indicating whether the client is currently in Group Pose (GPose) mode. /// public bool IsGPosing { get; } diff --git a/Dalamud/Plugin/Services/ISeStringEvaluator.cs b/Dalamud/Plugin/Services/ISeStringEvaluator.cs index 2bd423b7c..846dcd53e 100644 --- a/Dalamud/Plugin/Services/ISeStringEvaluator.cs +++ b/Dalamud/Plugin/Services/ISeStringEvaluator.cs @@ -32,6 +32,15 @@ public interface ISeStringEvaluator /// An evaluated . ReadOnlySeString Evaluate(ReadOnlySeStringSpan str, Span localParameters = default, ClientLanguage? language = null); + /// + /// Evaluates macros in a macro string. + /// + /// The macro string. + /// An optional list of local parameters. + /// An optional language override. + /// An evaluated . + ReadOnlySeString EvaluateMacroString(string macroString, Span localParameters = default, ClientLanguage? language = null); + /// /// Evaluates macros in text from the Addon sheet. /// diff --git a/Dalamud/Plugin/Services/ISigScanner.cs b/Dalamud/Plugin/Services/ISigScanner.cs index c0ebd9310..ac0f2c55f 100644 --- a/Dalamud/Plugin/Services/ISigScanner.cs +++ b/Dalamud/Plugin/Services/ISigScanner.cs @@ -10,12 +10,12 @@ namespace Dalamud.Game; public interface ISigScanner { /// - /// Gets a value indicating whether or not the search on this module is performed on a copy. + /// Gets a value indicating whether the search on this module is performed on a copy. /// public bool IsCopy { get; } /// - /// Gets a value indicating whether or not the ProcessModule is 32-bit. + /// Gets a value indicating whether the ProcessModule is 32-bit. /// public bool Is32BitProcess { get; } @@ -84,7 +84,7 @@ public interface ISigScanner /// The offset from function start of the instruction using the data. /// An IntPtr to the static memory location. public nint GetStaticAddressFromSig(string signature, int offset = 0); - + /// /// Try scanning for a .data address using a .text function. /// This is intended to be used with IDA sigs. @@ -95,14 +95,14 @@ public interface ISigScanner /// The offset from function start of the instruction using the data. /// true if the signature was found. public bool TryGetStaticAddressFromSig(string signature, out nint result, int offset = 0); - + /// /// Scan for a byte signature in the .data section. /// /// The signature. /// The real offset of the found signature. public nint ScanData(string signature); - + /// /// Try scanning for a byte signature in the .data section. /// @@ -110,14 +110,14 @@ public interface ISigScanner /// The real offset of the signature, if found. /// true if the signature was found. public bool TryScanData(string signature, out nint result); - + /// /// Scan for a byte signature in the whole module search area. /// /// The signature. /// The real offset of the found signature. public nint ScanModule(string signature); - + /// /// Try scanning for a byte signature in the whole module search area. /// @@ -125,7 +125,7 @@ public interface ISigScanner /// The real offset of the signature, if found. /// true if the signature was found. public bool TryScanModule(string signature, out nint result); - + /// /// Resolve a RVA address. /// @@ -133,14 +133,14 @@ public interface ISigScanner /// The relative offset. /// The calculated offset. public nint ResolveRelativeAddress(nint nextInstAddr, int relOffset); - + /// /// Scan for a byte signature in the .text section. /// /// The signature. /// The real offset of the found signature. public nint ScanText(string signature); - + /// /// Try scanning for a byte signature in the .text section. /// diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index ff13f11f1..3f9ae99df 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -9,6 +9,8 @@ using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; +using ImGuiNET; + using Lumina.Data.Files; namespace Dalamud.Plugin.Services; @@ -45,6 +47,14 @@ public interface ITextureProvider bool cpuWrite, string? debugName = null); + /// Creates a texture that can be drawn from an or an . + /// + /// Name for debug display purposes. + /// A new draw list texture. + /// No new resource is allocated upfront; it will be done when is + /// set with positive values for both components. + IDrawListTextureWrap CreateDrawListTexture(string? debugName = null); + /// Creates a texture from the given existing texture, cropping and converting pixel format as needed. /// /// The source texture wrap. The passed value may be disposed once this function returns, @@ -169,6 +179,14 @@ public interface ITextureProvider string? debugName = null, CancellationToken cancellationToken = default); + /// Creates a texture from clipboard. + /// Name for debug display purposes. + /// The cancellation token. + /// A representing the status of the operation. + Task CreateFromClipboardAsync( + string? debugName = null, + CancellationToken cancellationToken = default); + /// Gets the supported bitmap decoders. /// The supported bitmap decoders. /// @@ -191,6 +209,11 @@ public interface ITextureProvider /// Caching the returned object is not recommended. Performance benefit will be minimal. /// ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup); + + /// Gets a value indicating whether the current desktop clipboard contains an image that can be attempted + /// to read using . + /// true if it is the case. + bool HasClipboardImage(); /// Gets a shared texture corresponding to the given game resource icon specifier. /// @@ -200,7 +223,7 @@ public interface ITextureProvider /// /// A game icon specifier. /// The resulting . - /// Whether or not the lookup succeeded. + /// Whether the lookup succeeded. bool TryGetFromGameIcon(in GameIconLookup lookup, [NotNullWhen(true)] out ISharedImmediateTexture? texture); /// Gets a shared texture corresponding to the given path to a game resource. @@ -221,7 +244,7 @@ public interface ITextureProvider /// Caching the returned object is not recommended. Performance benefit will be minimal. /// ISharedImmediateTexture GetFromFile(string path); - + /// Gets a shared texture corresponding to the given file on the filesystem. /// The file on the filesystem to load. /// The shared texture that you may use to obtain the loaded texture wrap and load states. diff --git a/Dalamud/Plugin/Services/ITextureReadbackProvider.cs b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs index b41ded41f..3d2894355 100644 --- a/Dalamud/Plugin/Services/ITextureReadbackProvider.cs +++ b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs @@ -106,4 +106,17 @@ public interface ITextureReadbackProvider IReadOnlyDictionary? props = null, bool leaveWrapOpen = false, CancellationToken cancellationToken = default); + + /// Copies the texture to clipboard. + /// Texture wrap to copy. + /// Preferred file name. + /// Whether to leave non-disposed when the returned + /// completes. + /// The cancellation token. + /// A representing the status of the operation. + Task CopyToClipboardAsync( + IDalamudTextureWrap wrap, + string? preferredFileNameWithoutExtension = null, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default); } diff --git a/Dalamud/SafeMemory.cs b/Dalamud/SafeMemory.cs index 61f3cf82b..a8ac40a5d 100644 --- a/Dalamud/SafeMemory.cs +++ b/Dalamud/SafeMemory.cs @@ -25,7 +25,7 @@ public static class SafeMemory /// The address to read from. /// The amount of bytes to read. /// The result buffer. - /// Whether or not the read succeeded. + /// Whether the read succeeded. public static unsafe bool ReadBytes(IntPtr address, int count, out byte[] buffer) { buffer = new byte[count <= 0 ? 0 : count]; @@ -51,7 +51,7 @@ public static class SafeMemory /// /// The address to write to. /// The buffer to write. - /// Whether or not the write succeeded. + /// Whether the write succeeded. public static unsafe bool WriteBytes(IntPtr address, byte[] buffer) { if (buffer.Length == 0) @@ -80,7 +80,7 @@ public static class SafeMemory /// The type of the struct. /// The address to read from. /// The resulting object. - /// Whether or not the read succeeded. + /// Whether the read succeeded. public static bool Read(IntPtr address, out T result) where T : struct { if (!ReadBytes(address, SizeCache.Size, out var buffer)) @@ -122,7 +122,7 @@ public static class SafeMemory /// The type of the struct. /// The address to write to. /// The object to write. - /// Whether or not the write succeeded. + /// Whether the write succeeded. public static bool Write(IntPtr address, T obj) where T : struct { using var mem = new LocalMemory(SizeCache.Size); @@ -136,7 +136,7 @@ public static class SafeMemory /// The type of the structs. /// The address to write to. /// The array to write. - /// Whether or not the write succeeded. + /// Whether the write succeeded. public static bool Write(IntPtr address, T[] objArray) where T : struct { if (objArray == null || objArray.Length == 0) @@ -195,7 +195,7 @@ public static class SafeMemory /// /// The address to write to. /// The string to write. - /// Whether or not the write succeeded. + /// Whether the write succeeded. public static bool WriteString(IntPtr address, string str) { return WriteString(address, str, Encoding.UTF8); @@ -212,7 +212,7 @@ public static class SafeMemory /// The address to write to. /// The string to write. /// The encoding to use. - /// Whether or not the write succeeded. + /// Whether the write succeeded. public static bool WriteString(IntPtr address, string str, Encoding encoding) { if (string.IsNullOrEmpty(str)) diff --git a/Dalamud/Service/ServiceManager.cs b/Dalamud/Service/ServiceManager.cs index 206b24736..9847f7147 100644 --- a/Dalamud/Service/ServiceManager.cs +++ b/Dalamud/Service/ServiceManager.cs @@ -72,7 +72,7 @@ internal static class ServiceManager /// The justification for using this feature. [InjectableType] public delegate void RegisterUnloadAfterDelegate(IEnumerable unloadAfter, string justification); - + /// /// Kinds of services. /// @@ -83,27 +83,27 @@ internal static class ServiceManager /// Not a service. /// None = 0, - + /// /// Service that is loaded manually. /// ProvidedService = 1 << 0, - + /// /// Service that is loaded asynchronously while the game starts. /// EarlyLoadedService = 1 << 1, - + /// /// Service that is loaded before the game starts. /// BlockingEarlyLoadedService = 1 << 2, - + /// /// Service that is only instantiable via scopes. /// ScopedService = 1 << 3, - + /// /// Service that is loaded automatically when the game starts, synchronously or asynchronously. /// @@ -114,7 +114,7 @@ internal static class ServiceManager /// Gets task that gets completed when all blocking early loading services are done loading. /// public static Task BlockingResolved { get; } = BlockingServicesLoadedTaskCompletionSource.Task; - + /// /// Gets a cancellation token that will be cancelled once Dalamud needs to unload, be it due to a failure state /// during initialization or during regular operation. @@ -136,17 +136,25 @@ internal static class ServiceManager TargetSigScanner scanner, Localization localization) { -#if DEBUG - lock (LoadedServices) + void ProvideAllServices() { + // ServiceContainer MUST be first. The static ctor of Service will call Service.Get() + // which causes a deadlock otherwise. + ProvideService(new ServiceContainer()); + ProvideService(dalamud); ProvideService(fs); ProvideService(configuration); - ProvideService(new ServiceContainer()); ProvideService(scanner); ProvideService(localization); } +#if DEBUG + lock (LoadedServices) + { + ProvideAllServices(); + } + return; void ProvideService(T service) where T : IServiceType @@ -156,12 +164,8 @@ internal static class ServiceManager LoadedServices.Add(typeof(T)); } #else - ProvideService(dalamud); - ProvideService(fs); - ProvideService(configuration); - ProvideService(new ServiceContainer()); - ProvideService(scanner); - ProvideService(localization); + + ProvideAllServices(); return; void ProvideService(T service) where T : IServiceType => Service.Provide(service); @@ -193,7 +197,7 @@ internal static class ServiceManager var getAsyncTaskMap = new Dictionary(); var serviceContainer = Service.Get(); - + foreach (var serviceType in GetConcreteServiceTypes()) { var serviceKind = serviceType.GetServiceKind(); @@ -202,13 +206,13 @@ internal static class ServiceManager // Let IoC know about the interfaces this service implements serviceContainer.RegisterInterfaces(serviceType); - + // Scoped service do not go through Service and are never early loaded if (serviceKind.HasFlag(ServiceKind.ScopedService)) continue; var genericWrappedServiceType = typeof(Service<>).MakeGenericType(serviceType); - + var getTask = (Task)genericWrappedServiceType .InvokeMember( nameof(Service.GetAsync), @@ -290,7 +294,7 @@ internal static class ServiceManager var tasks = tasksEnumerable.AsReadOnlyCollection(); if (tasks.Count == 0) return; - + // Time we wait until showing the loading dialog const int loadingDialogTimeout = 10000; @@ -330,7 +334,7 @@ internal static class ServiceManager hasDeps = false; } } - + if (!hasDeps) continue; @@ -437,7 +441,7 @@ internal static class ServiceManager public static void UnloadAllServices() { UnloadCancellationTokenSource.Cancel(); - + var framework = Service.GetNullable(Service.ExceptionPropagationMode.None); if (framework is { IsInFrameworkUpdateThread: false, IsFrameworkUnloading: false }) { @@ -450,14 +454,14 @@ internal static class ServiceManager var dependencyServicesMap = new Dictionary>(); var allToUnload = new HashSet(); var unloadOrder = new List(); - + Log.Information("==== COLLECTING SERVICES TO UNLOAD ===="); - + foreach (var serviceType in GetConcreteServiceTypes()) { if (!serviceType.IsAssignableTo(typeof(IServiceType))) continue; - + // Scoped services shall never be unloaded here. // Their lifetime must be managed by the IServiceScope that owns them. If it leaks, it's their fault. if (serviceType.GetServiceKind() == ServiceKind.ScopedService) @@ -485,12 +489,12 @@ internal static class ServiceManager unloadOrder.Add(serviceType); Log.Information("Queue for unload {Type}", serviceType.FullName!); } - + foreach (var serviceType in allToUnload) { UnloadService(serviceType); } - + Log.Information("==== UNLOADING ALL SERVICES ===="); unloadOrder.Reverse(); @@ -507,7 +511,7 @@ internal static class ServiceManager null, null); } - + #if DEBUG lock (LoadedServices) { @@ -536,17 +540,17 @@ internal static class ServiceManager var attr = type.GetCustomAttribute(true)?.GetType(); if (attr == null) return ServiceKind.None; - + Debug.Assert( type.IsAssignableTo(typeof(IServiceType)), "Service did not inherit from IServiceType"); if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedServiceAttribute))) return ServiceKind.BlockingEarlyLoadedService; - + if (attr.IsAssignableTo(typeof(EarlyLoadedServiceAttribute))) return ServiceKind.EarlyLoadedService; - + if (attr.IsAssignableTo(typeof(ScopedServiceAttribute))) return ServiceKind.ScopedService; @@ -572,7 +576,7 @@ internal static class ServiceManager var isAnyDisposable = isServiceDisposable || serviceType.IsAssignableTo(typeof(IDisposable)) - || serviceType.IsAssignableTo(typeof(IAsyncDisposable)); + || serviceType.IsAssignableTo(typeof(IAsyncDisposable)); if (isAnyDisposable && !isServiceDisposable) { throw new InvalidOperationException( diff --git a/Dalamud/Service/Service{T}.cs b/Dalamud/Service/Service{T}.cs index b4bfff917..1f5558893 100644 --- a/Dalamud/Service/Service{T}.cs +++ b/Dalamud/Service/Service{T}.cs @@ -23,6 +23,9 @@ namespace Dalamud; [SuppressMessage("ReSharper", "StaticMemberInGenericType", Justification = "Service container static type")] internal static class Service where T : IServiceType { + // TODO: Service should only work with singleton services. Trying to call Service.Get() on a scoped service should + // be a compile-time error. + private static readonly ServiceManager.ServiceAttribute ServiceAttribute; private static TaskCompletionSource instanceTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); private static List? dependencyServices; @@ -42,8 +45,13 @@ internal static class Service where T : IServiceType else ServiceManager.Log.Debug("Service<{0}>: Static ctor called", type.Name); - if (exposeToPlugins) - Service.Get().RegisterSingleton(instanceTcs.Task); + // We can't use the service container to register itself. It does so in its constructor. + if (typeof(T) != typeof(ServiceContainer)) + { + Service.Get().RegisterSingleton( + instanceTcs.Task, + exposeToPlugins ? ObjectInstanceVisibility.ExposedToPlugins : ObjectInstanceVisibility.Internal); + } } /// @@ -163,7 +171,7 @@ internal static class Service where T : IServiceType return dependencyServices; var res = new List(); - + ServiceManager.Log.Verbose("Service<{0}>: Getting dependencies", typeof(T).Name); var ctor = GetServiceConstructor(); @@ -174,12 +182,12 @@ internal static class Service where T : IServiceType .Select(x => x.ParameterType) .Where(x => x.GetServiceKind() != ServiceManager.ServiceKind.None)); } - + res.AddRange(typeof(T) .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(x => x.GetCustomAttribute(true) != null) .Select(x => x.FieldType)); - + res.AddRange(typeof(T) .GetCustomAttributes() .OfType() @@ -351,7 +359,7 @@ internal static class Service where T : IServiceType var ctor = GetServiceConstructor(); if (ctor == null) throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor"); - + var args = await ResolveInjectedParameters(ctor.GetParameters(), additionalProvidedTypedObjects) .ConfigureAwait(false); using (Timings.Start($"{typeof(T).Name} Construct")) @@ -387,7 +395,7 @@ internal static class Service where T : IServiceType argTask = Task.FromResult(additionalProvidedTypedObjects.Single(x => x.GetType() == argType)); continue; } - + argTask = (Task)typeof(Service<>) .MakeGenericType(argType) .InvokeMember( diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index 9b87a71a0..9791b9e45 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -157,7 +157,7 @@ internal class ReliableFileStorage : IInternalDisposableService /// automatically written back to disk, however. /// /// The path to read from. - /// Whether or not the backup of the file should take priority. + /// Whether the backup of the file should take priority. /// The container to read from. /// All text stored in this file. /// Thrown if the file does not exist on the filesystem or in the backup. @@ -171,7 +171,7 @@ internal class ReliableFileStorage : IInternalDisposableService /// /// The path to read from. /// The encoding to read with. - /// Whether or not the backup of the file should take priority. + /// Whether the backup of the file should take priority. /// The container to read from. /// All text stored in this file. /// Thrown if the file does not exist on the filesystem or in the backup. @@ -249,7 +249,7 @@ internal class ReliableFileStorage : IInternalDisposableService /// automatically written back to disk, however. /// /// The path to read from. - /// Whether or not the backup of the file should take priority. + /// Whether the backup of the file should take priority. /// The container to read from. /// All bytes stored in this file. /// Thrown if the file does not exist on the filesystem or in the backup. diff --git a/Dalamud/Support/BugBait.cs b/Dalamud/Support/BugBait.cs index c82e5e652..8dbf2e429 100644 --- a/Dalamud/Support/BugBait.cs +++ b/Dalamud/Support/BugBait.cs @@ -20,10 +20,10 @@ internal static class BugBait /// Send feedback to Discord. /// /// The plugin to send feedback about. - /// Whether or not the plugin is a testing plugin. + /// Whether the plugin is a testing plugin. /// The content of the feedback. /// The reporter name. - /// Whether or not the most recent exception to occur should be included in the report. + /// Whether the most recent exception to occur should be included in the report. /// A representing the asynchronous operation. public static async Task SendFeedback(IPluginManifest plugin, bool isTesting, string content, string reporter, bool includeException) { @@ -43,7 +43,7 @@ internal static class BugBait { model.Exception = Troubleshooting.LastException == null ? "Was included, but none happened" : Troubleshooting.LastException?.ToString(); } - + var httpClient = Service.Get().SharedHttpClient; var postContent = new StringContent(JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json"); diff --git a/Dalamud/Utility/Api12ToDoAttribute.cs b/Dalamud/Utility/Api12ToDoAttribute.cs deleted file mode 100644 index 9f871274d..000000000 --- a/Dalamud/Utility/Api12ToDoAttribute.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Dalamud.Utility; - -/// -/// Utility class for marking something to be changed for API 11, for ease of lookup. -/// -[AttributeUsage(AttributeTargets.All, Inherited = false)] -internal sealed class Api12ToDoAttribute : Attribute -{ - /// - /// Marks that this should be made internal. - /// - public const string MakeInternal = "Make internal."; - - /// - /// Initializes a new instance of the class. - /// - /// The explanation. - /// The explanation 2. - public Api12ToDoAttribute(string what, string what2 = "") - { - _ = what; - _ = what2; - } -} diff --git a/Dalamud/Utility/ClipboardFormats.cs b/Dalamud/Utility/ClipboardFormats.cs new file mode 100644 index 000000000..07b6c00d6 --- /dev/null +++ b/Dalamud/Utility/ClipboardFormats.cs @@ -0,0 +1,40 @@ +using System.Runtime.InteropServices; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Utility; + +/// Clipboard formats, looked up by their names. +internal static class ClipboardFormats +{ + /// + public static uint FileContents { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILECONTENTS); + + /// Gets the clipboard format corresponding to the PNG file format. + public static uint Png { get; } = ClipboardFormatFromName("PNG"); + + /// + public static uint FileDescriptorW { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILEDESCRIPTORW); + + /// + public static uint FileDescriptorA { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILEDESCRIPTORA); + + /// + public static uint FileNameW { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILENAMEW); + + /// + public static uint FileNameA { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILENAMEA); + + private static unsafe uint ClipboardFormatFromName(ReadOnlySpan name) + { + uint cf; + fixed (void* p = name) + cf = RegisterClipboardFormatW((ushort*)p); + if (cf != 0) + return cf; + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ?? + new InvalidOperationException($"RegisterClipboardFormatW({name}) failed."); + } +} diff --git a/Dalamud/Utility/ItemUtil.cs b/Dalamud/Utility/ItemUtil.cs index 32160aa15..0b37a6abb 100644 --- a/Dalamud/Utility/ItemUtil.cs +++ b/Dalamud/Utility/ItemUtil.cs @@ -14,14 +14,14 @@ namespace Dalamud.Utility; /// /// Utilities related to Items. /// -internal static class ItemUtil +public static class ItemUtil { private static int? eventItemRowCount; /// Converts raw item ID to item ID with its classification. /// Raw item ID. /// Item ID and its classification. - internal static (uint ItemId, ItemKind Kind) GetBaseId(uint rawItemId) + public static (uint ItemId, ItemKind Kind) GetBaseId(uint rawItemId) { if (IsEventItem(rawItemId)) return (rawItemId, ItemKind.EventItem); // EventItem IDs are NOT adjusted if (IsHighQuality(rawItemId)) return (rawItemId - 1_000_000, ItemKind.Hq); @@ -33,7 +33,7 @@ internal static class ItemUtil /// Item ID. /// Item classification. /// Raw Item ID. - internal static uint GetRawId(uint itemId, ItemKind kind) + public static uint GetRawId(uint itemId, ItemKind kind) { return kind switch { @@ -50,7 +50,7 @@ internal static class ItemUtil /// The item id to check. /// true when the item id belongs to a normal item. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsNormalItem(uint itemId) + public static bool IsNormalItem(uint itemId) { return itemId < 500_000; } @@ -61,7 +61,7 @@ internal static class ItemUtil /// The item id to check. /// true when the item id belongs to a collectible item. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsCollectible(uint itemId) + public static bool IsCollectible(uint itemId) { return itemId is >= 500_000 and < 1_000_000; } @@ -72,7 +72,7 @@ internal static class ItemUtil /// The item id to check. /// true when the item id belongs to a high quality item. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsHighQuality(uint itemId) + public static bool IsHighQuality(uint itemId) { return itemId is >= 1_000_000 and < 2_000_000; } @@ -83,7 +83,7 @@ internal static class ItemUtil /// The item id to check. /// true when the item id belongs to an event item. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsEventItem(uint itemId) + public static bool IsEventItem(uint itemId) { return itemId >= 2_000_000 && itemId - 2_000_000 < (eventItemRowCount ??= Service.Get().GetExcelSheet().Count); } @@ -95,7 +95,7 @@ internal static class ItemUtil /// Whether to include the High Quality or Collectible icon. /// An optional client language override. /// The item name. - internal static ReadOnlySeString GetItemName(uint itemId, bool includeIcon = true, ClientLanguage? language = null) + public static ReadOnlySeString GetItemName(uint itemId, bool includeIcon = true, ClientLanguage? language = null) { var dataManager = Service.Get(); @@ -145,7 +145,7 @@ internal static class ItemUtil /// The raw item Id. /// Wheather this color is used as edge color. /// The Color row id. - internal static uint GetItemRarityColorType(uint itemId, bool isEdgeColor = false) + public static uint GetItemRarityColorType(uint itemId, bool isEdgeColor = false) { var rarity = 1u; diff --git a/Dalamud/Utility/Signatures/NullabilityUtil.cs b/Dalamud/Utility/Signatures/NullabilityUtil.cs index da4b24db6..da45db197 100755 --- a/Dalamud/Utility/Signatures/NullabilityUtil.cs +++ b/Dalamud/Utility/Signatures/NullabilityUtil.cs @@ -14,21 +14,21 @@ internal static class NullabilityUtil /// Check if the provided property is nullable. /// /// The property to check. - /// Whether or not the property is nullable. + /// Whether the property is nullable. internal static bool IsNullable(PropertyInfo property) => IsNullableHelper(property.PropertyType, property.DeclaringType, property.CustomAttributes); /// /// Check if the provided field is nullable. /// /// The field to check. - /// Whether or not the field is nullable. + /// Whether the field is nullable. internal static bool IsNullable(FieldInfo field) => IsNullableHelper(field.FieldType, field.DeclaringType, field.CustomAttributes); /// /// Check if the provided parameter is nullable. /// /// The parameter to check. - /// Whether or not the parameter is nullable. + /// Whether the parameter is nullable. internal static bool IsNullable(ParameterInfo parameter) => IsNullableHelper(parameter.ParameterType, parameter.Member, parameter.CustomAttributes); private static bool IsNullableHelper(Type memberType, MemberInfo? declaringType, IEnumerable customAttributes) diff --git a/Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs b/Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs index 76e32da28..a660164c1 100755 --- a/Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs +++ b/Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs @@ -16,7 +16,7 @@ internal interface IFieldOrPropertyInfo Type ActualType { get; } /// - /// Gets a value indicating whether or not the field or property is nullable. + /// Gets a value indicating whether the field or property is nullable. /// bool IsNullable { get; } diff --git a/Dalamud/Utility/StringExtensions.cs b/Dalamud/Utility/StringExtensions.cs index 50973e338..c28aebab2 100644 --- a/Dalamud/Utility/StringExtensions.cs +++ b/Dalamud/Utility/StringExtensions.cs @@ -1,5 +1,8 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Text; + +using Dalamud.Game; using FFXIVClientStructs.FFXIV.Client.UI; @@ -10,6 +13,9 @@ namespace Dalamud.Utility; /// public static class StringExtensions { + private static readonly string[] CommonExcludedWords = ["sas", "zos", "van", "nan", "tol", "deus", "mal", "de", "rem", "out", "yae", "bas", "cen", "quo", "viator", "la"]; + private static readonly string[] EnglishExcludedWords = ["of", "the", "to", "and", "a", "an", "or", "at", "by", "for", "in", "on", "with", "from", .. CommonExcludedWords]; + /// /// An extension method to chain usage of string.Format. /// @@ -77,7 +83,7 @@ public static class StringExtensions public static string StripSoftHyphen(this string input) => input.Replace("\u00AD", string.Empty); /// - /// Truncates the given string to the specified maximum number of characters, + /// Truncates the given string to the specified maximum number of characters, /// appending an ellipsis if truncation occurs. /// /// The string to truncate. @@ -88,4 +94,128 @@ public static class StringExtensions { return string.IsNullOrEmpty(input) || input.Length <= maxChars ? input : input[..maxChars] + ellipses; } + + /// + /// Converts the input string to uppercase based on specified options like capitalizing the first character, + /// normalizing vowels, and excluding certain words based on the selected language. + /// + /// The input string to be converted to uppercase. + /// Whether to capitalize only the first character of the string. + /// Whether to capitalize the first letter of each word. + /// Whether to normalize vowels to uppercase if they appear at the beginning of a word. + /// The language context used to determine which words to exclude from capitalization. + /// A new string with the appropriate characters converted to uppercase. + /// This is a C# implementation of Client::System::String::Utf8String.ToUpper with word exclusion lists as used by the HeadAll macro. + public static string ToUpper(this string input, bool firstCharOnly, bool everyWord, bool normalizeVowels, ClientLanguage language) + { + return ToUpper(input, firstCharOnly, everyWord, normalizeVowels, language switch + { + ClientLanguage.Japanese => [], + ClientLanguage.English => EnglishExcludedWords, + ClientLanguage.German => CommonExcludedWords, + ClientLanguage.French => CommonExcludedWords, + _ => [], + }); + } + + /// + /// Converts the input string to uppercase based on specified options like capitalizing the first character, + /// normalizing vowels, and excluding certain words based on the selected language. + /// + /// The input string to be converted to uppercase. + /// Whether to capitalize only the first character of the string. + /// Whether to capitalize the first letter of each word. + /// Whether to normalize vowels to uppercase if they appear at the beginning of a word. + /// A list of words to exclude from being capitalized. Words in this list will remain lowercase. + /// A new string with the appropriate characters converted to uppercase. + /// This is a C# implementation of Client::System::String::Utf8String.ToUpper. + public static string ToUpper(this string input, bool firstCharOnly, bool everyWord, bool normalizeVowels, ReadOnlySpan excludedWords) + { + if (string.IsNullOrEmpty(input)) + return input; + + var builder = new StringBuilder(input); + var isWordBeginning = true; + var length = firstCharOnly && !everyWord ? 1 : builder.Length; + + for (var i = 0; i < length; i++) + { + var ch = builder[i]; + + if (ch == ' ') + { + isWordBeginning = true; + continue; + } + + if (firstCharOnly && !isWordBeginning) + continue; + + // Basic ASCII a-z + if (ch >= 'a' && ch <= 'z') + { + var substr = builder.ToString(i, builder.Length - i); + var isExcluded = false; + + // Do not exclude words at the beginning + if (i > 0) + { + foreach (var excludedWord in excludedWords) + { + if (substr.StartsWith(excludedWord + " ", StringComparison.OrdinalIgnoreCase)) + { + isExcluded = true; + break; + } + } + } + + if (!isExcluded) + { + builder[i] = char.ToUpperInvariant(ch); + } + } + + // Special œ → Œ + else if (ch == 'œ') + { + builder[i] = 'Œ'; + } + + // Characters with accents + else if (ch >= 'à' && ch <= 'ý' && ch != '÷') + { + builder[i] = char.ToUpperInvariant(ch); + } + + // Normalize vowels with accents + else if (normalizeVowels && isWordBeginning) + { + if ("àáâãäå".Contains(ch)) + { + builder[i] = 'A'; + } + else if ("èéêë".Contains(ch)) + { + builder[i] = 'E'; + } + else if ("ìíîï".Contains(ch)) + { + builder[i] = 'I'; + } + else if ("òóôõö".Contains(ch)) + { + builder[i] = 'O'; + } + else if ("ùúûü".Contains(ch)) + { + builder[i] = 'U'; + } + } + + isWordBeginning = false; + } + + return builder.ToString(); + } } diff --git a/Dalamud/Utility/ThreadBoundTaskScheduler.cs b/Dalamud/Utility/ThreadBoundTaskScheduler.cs index 4b6de29ff..2930bd27f 100644 --- a/Dalamud/Utility/ThreadBoundTaskScheduler.cs +++ b/Dalamud/Utility/ThreadBoundTaskScheduler.cs @@ -22,8 +22,14 @@ internal class ThreadBoundTaskScheduler : TaskScheduler public ThreadBoundTaskScheduler(Thread? boundThread = null) { this.BoundThread = boundThread; + this.TaskQueued += static () => { }; } + /// + /// Event fired when a task has been posted. + /// + public event Action TaskQueued; + /// /// Gets or sets the thread this task scheduler is bound to. /// @@ -57,6 +63,7 @@ internal class ThreadBoundTaskScheduler : TaskScheduler /// protected override void QueueTask(Task task) { + this.TaskQueued.Invoke(); this.scheduledTasks[task] = Scheduled; } diff --git a/Dalamud/Utility/Timing/TimingHandle.cs b/Dalamud/Utility/Timing/TimingHandle.cs index d73a9c2d3..e334908b1 100644 --- a/Dalamud/Utility/Timing/TimingHandle.cs +++ b/Dalamud/Utility/Timing/TimingHandle.cs @@ -67,7 +67,7 @@ public sealed class TimingHandle : TimingEvent, IDisposable, IComparable - /// Gets a value indicating whether or not this timing was started on the main thread. + /// Gets a value indicating whether this timing was started on the main thread. /// public bool IsMainThread { get; private set; } diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 3ef1fb2fd..4edd89c54 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -70,10 +70,17 @@ public static class Util private static ulong moduleEndAddr; /// - /// Gets the assembly version of Dalamud. + /// Gets the Dalamud version. /// + [Api13ToDo("Remove. Make both versions here internal. Add an API somewhere.")] public static string AssemblyVersion { get; } = - Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString(); + Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!.ToString(); + + /// + /// Gets the Dalamud version. + /// + internal static Version AssemblyVersionParsed { get; } = + Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!; /// /// Gets the SCM Version from the assembly, or null if it cannot be found. This method will generally return @@ -308,7 +315,7 @@ public static class Util /// /// The structure to show. /// The address to the structure. - /// Whether or not this structure should start out expanded. + /// Whether this structure should start out expanded. /// The already followed path. public static void ShowStruct(object obj, ulong addr, bool autoExpand = false, IEnumerable? path = null) => ShowStructInternal(obj, addr, autoExpand, path); @@ -318,7 +325,7 @@ public static class Util /// /// The type of the structure. /// The pointer to the structure. - /// Whether or not this structure should start out expanded. + /// Whether this structure should start out expanded. public static unsafe void ShowStruct(T* obj, bool autoExpand = false) where T : unmanaged { ShowStruct(*obj, (ulong)&obj, autoExpand); @@ -328,7 +335,7 @@ public static class Util /// Show a GameObject's internal data in an ImGui-context. /// /// The GameObject to show. - /// Whether or not the struct should start as expanded. + /// Whether the struct should start as expanded. public static unsafe void ShowGameObjectStruct(IGameObject go, bool autoExpand = true) { switch (go) @@ -612,7 +619,7 @@ public static class Util uCount = uint.MaxValue, dwTimeout = 0, dwFlags = FLASHWINFO_FLAGS.FLASHW_ALL | FLASHWINFO_FLAGS.FLASHW_TIMERNOFG, - hwnd = new HWND(Process.GetCurrentProcess().MainWindowHandle.ToPointer()), + hwnd = new HWND(Process.GetCurrentProcess().MainWindowHandle), }; Win32_PInvoke.FlashWindowEx(flashInfo); } @@ -1050,7 +1057,7 @@ public static class Util /// /// The structure to show. /// The address to the structure. - /// Whether or not this structure should start out expanded. + /// Whether this structure should start out expanded. /// The already followed path. /// Do not print addresses. Use when displaying a copied value. private static void ShowStructInternal(object obj, ulong addr, bool autoExpand = false, IEnumerable? path = null, bool hideAddress = false) diff --git a/DalamudCrashHandler/miniz.h b/DalamudCrashHandler/miniz.h index 6cc398c92..0c12a3ccb 100644 --- a/DalamudCrashHandler/miniz.h +++ b/DalamudCrashHandler/miniz.h @@ -115,7 +115,7 @@ -/* Defines to completely disable specific portions of miniz.c: +/* Defines to completely disable specific portions of miniz.c: If all macros here are defined the only functionality remaining will be CRC-32, adler-32, tinfl, and tdefl. */ /* Define MINIZ_NO_STDIO to disable all usage and any functions which rely on stdio for file I/O. */ @@ -138,7 +138,7 @@ /* Define MINIZ_NO_ZLIB_COMPATIBLE_NAME to disable zlib names, to prevent conflicts against stock zlib. */ /*#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES */ -/* Define MINIZ_NO_MALLOC to disable all calls to malloc, free, and realloc. +/* Define MINIZ_NO_MALLOC to disable all calls to malloc, free, and realloc. Note if MINIZ_NO_MALLOC is defined then the user must always provide custom user alloc/free/realloc callbacks to the zlib and archive API's, and a few stand-alone helper API's which don't provide custom user functions (such as tdefl_compress_mem_to_heap() and tinfl_decompress_mem_to_heap()) won't work. */ @@ -360,7 +360,7 @@ MINIZ_EXPORT mz_ulong mz_compressBound(mz_ulong source_len); /* Initializes a decompressor. */ MINIZ_EXPORT int mz_inflateInit(mz_streamp pStream); -/* mz_inflateInit2() is like mz_inflateInit() with an additional option that controls the window size and whether or not the stream has been wrapped with a zlib header/footer: */ +/* mz_inflateInit2() is like mz_inflateInit() with an additional option that controls the window size and whether the stream has been wrapped with a zlib header/footer: */ /* window_bits must be MZ_DEFAULT_WINDOW_BITS (to parse zlib header/footer) or -MZ_DEFAULT_WINDOW_BITS (raw deflate). */ MINIZ_EXPORT int mz_inflateInit2(mz_streamp pStream, int window_bits); @@ -908,7 +908,7 @@ struct tinfl_decompressor_tag #ifdef __cplusplus } #endif - + #pragma once diff --git a/Directory.Build.props b/Directory.Build.props index ef07620a4..92902b0c0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,8 +10,8 @@ - 5.6.1 - 7.2.1 + 5.7.0 + 7.2.2 13.0.3 diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 9e7f03ed6..a93b68f50 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 9e7f03ed6d3d5cb9e6952f00c4779ac64427bc81 +Subproject commit a93b68f501556f644f97da2d0a54dba83b5ffda4 diff --git a/lib/cimgui b/lib/cimgui index e29ccc495..27c8565f6 160000 --- a/lib/cimgui +++ b/lib/cimgui @@ -1 +1 @@ -Subproject commit e29ccc49518b2e106ef9aef47dc5d22c5e51558f +Subproject commit 27c8565f631b004c3266373890e41ecc627f775b