From e6da98646a554b87b6373b573060592ebe1023e2 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Thu, 6 Jun 2024 10:26:00 -0700 Subject: [PATCH 01/16] feat: Expose more commands in help (#1824) --- Dalamud/Interface/Internal/DalamudCommands.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index 9e6f7cf32..18936687a 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -25,19 +25,19 @@ internal class DalamudCommands : IServiceType { commandManager.AddHandler("/xldclose", new CommandInfo(this.OnUnloadCommand) { - HelpMessage = Loc.Localize("DalamudUnloadHelp", "Unloads XIVLauncher in-game addon."), + HelpMessage = Loc.Localize("DalamudUnloadHelp", "Unloads XIVLauncher in-game addon. For debug use only!"), ShowInHelp = false, }); commandManager.AddHandler("/xlkill", new CommandInfo(this.OnKillCommand) { - HelpMessage = "Kill the game.", + HelpMessage = "Kill the game. For debug use only!", ShowInHelp = false, }); commandManager.AddHandler("/xlrestart", new CommandInfo(this.OnRestartCommand) { - HelpMessage = "Restart the game.", + HelpMessage = "Restart the game. For debug use only!", ShowInHelp = false, }); @@ -80,13 +80,11 @@ internal class DalamudCommands : IServiceType commandManager.AddHandler("/xlstats", new CommandInfo(this.OnTogglePluginStats) { HelpMessage = Loc.Localize("DalamudPluginStats", "Draw plugin statistics window"), - ShowInHelp = false, }); commandManager.AddHandler("/xlbranch", new CommandInfo(this.OnToggleBranchSwitcher) { - HelpMessage = Loc.Localize("DalamudBranchSwitcher", "Draw branch switcher"), - ShowInHelp = false, + HelpMessage = Loc.Localize("DalamudBranchSwitcher", "Open the branch switcher"), }); commandManager.AddHandler("/xldata", new CommandInfo(this.OnDebugDrawDataMenu) @@ -97,8 +95,7 @@ internal class DalamudCommands : IServiceType commandManager.AddHandler("/xllog", new CommandInfo(this.OnOpenLog) { - HelpMessage = Loc.Localize("DalamudDevLogHelp", "Open dev log DEBUG"), - ShowInHelp = false, + HelpMessage = Loc.Localize("DalamudDevLogHelp", "Open the plugin log window/console"), }); commandManager.AddHandler("/xlplugins", new CommandInfo(this.OnOpenInstallerCommand) From 16f0fb76f4c7d074a3434551bb93e22f1966f741 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 7 Jun 2024 01:21:13 +0200 Subject: [PATCH 02/16] add basic console system for debugging --- Dalamud/Console/ConsoleArgumentType.cs | 27 + Dalamud/Console/ConsoleEntry.cs | 44 ++ Dalamud/Console/ConsoleManager.cs | 507 ++++++++++++++++++ Dalamud/Game/Command/CommandManager.cs | 11 + .../Interface/Internal/DalamudInterface.cs | 7 +- .../Internal/Windows/ConsoleWindow.cs | 34 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 13 +- Dalamud/Logging/Internal/ModuleLog.cs | 9 +- 8 files changed, 634 insertions(+), 18 deletions(-) create mode 100644 Dalamud/Console/ConsoleArgumentType.cs create mode 100644 Dalamud/Console/ConsoleEntry.cs create mode 100644 Dalamud/Console/ConsoleManager.cs diff --git a/Dalamud/Console/ConsoleArgumentType.cs b/Dalamud/Console/ConsoleArgumentType.cs new file mode 100644 index 000000000..4b4a74ce4 --- /dev/null +++ b/Dalamud/Console/ConsoleArgumentType.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Console; + +/// +/// Possible console argument types. +/// +internal enum ConsoleArgumentType +{ + /// + /// A regular string. + /// + String, + + /// + /// A signed integer. + /// + Integer, + + /// + /// A floating point value. + /// + Float, + + /// + /// A boolean value. + /// + Bool, +} diff --git a/Dalamud/Console/ConsoleEntry.cs b/Dalamud/Console/ConsoleEntry.cs new file mode 100644 index 000000000..701a4e04b --- /dev/null +++ b/Dalamud/Console/ConsoleEntry.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +namespace Dalamud.Console; + +/// +/// Interface representing an entry in the console. +/// +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. +/// +public interface IConsoleCommand : IConsoleEntry +{ + /// + /// Execute this command. + /// + /// Arguments to invoke the entry with. + /// Whether or not execution succeeded. + public bool Invoke(IEnumerable arguments); +} + +/// +/// Interface representing a variable in the console. +/// +/// The type of the variable +public interface IConsoleVariable : IConsoleEntry +{ + /// + /// Gets or sets the value of this variable. + /// + T Value { get; set; } +} diff --git a/Dalamud/Console/ConsoleManager.cs b/Dalamud/Console/ConsoleManager.cs new file mode 100644 index 000000000..17997cc59 --- /dev/null +++ b/Dalamud/Console/ConsoleManager.cs @@ -0,0 +1,507 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; + +using Dalamud.Logging.Internal; + +using Serilog.Events; + +namespace Dalamud.Console; + +// TODO: Mayhaps overloads with Func for commands? + +/// +/// Class managing console commands and variables. +/// +[ServiceManager.BlockingEarlyLoadedService("Console is needed by other blocking early loaded services.")] +internal partial class ConsoleManager : IServiceType +{ + private static readonly ModuleLog Log = new("CON"); + + private Dictionary entries = new(); + + /// + /// Initializes a new instance of the class. + /// + [ServiceManager.ServiceConstructor] + public ConsoleManager() + { + } + + /// + /// 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. + /// + /// The name of the command. + /// A description of the command. + /// Function to invoke when the command has been called. Must return a indicating success. + /// The added command. + /// Thrown if the command already exists. + public IConsoleCommand AddCommand(string name, string description, Delegate func) + { + 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; + } + + /// + /// Add a variable to the console. + /// + /// The name of the variable. + /// A description of the variable. + /// The default value of the variable. + /// The type of the variable. + /// The added variable. + /// Thrown if the variable already exists. + public IConsoleVariable AddVariable(string name, string description, T defaultValue) + { + 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; + } + + /// + /// Add an alias to a console entry. + /// + /// The name of the entry to add an alias for. + /// The alias to use. + /// The added alias. + public IConsoleEntry AddAlias(string name, string alias) + { + 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."); + + var aliasEntry = new ConsoleAlias(name, target); + this.entries.Add(alias, aliasEntry); + + return aliasEntry; + } + + /// + /// Remove an entry from the console. + /// + /// The entry to remove. + public void RemoveEntry(IConsoleEntry entry) + { + ArgumentNullException.ThrowIfNull(entry); + } + + /// + /// Get the value of a variable. + /// + /// The name of the variable. + /// The type of the variable. + /// The value of the variable. + /// Thrown if the variable could not be found. + /// Thrown if the found console entry is not of the expected type. + 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. + /// + /// The name of the variable. + /// The value to set. + /// The type of the value to set. + /// Thrown if the found console entry is not of the expected type. + /// Thrown if the variable could not be found. + public void SetVariable(string name, T value) + { + 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 InvalidOperationException($"Command '{name}' is not a variable."); + } + + /// + /// Process a console command. + /// + /// The command to process. + /// Whether or not 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(x => x == ' ')) + { + Log.Error("No valid command specified"); + return false; + } + + var entry = this.FindEntry(entryName); + if (entry == null) + { + Log.Error("Command '{CommandName}' not found", entryName); + return false; + } + + var parsedArguments = new List(); + + if (entry.ValidArguments != null) + { + for (var i = 1; i < matches.Count; i++) + { + if (i - 1 >= entry.ValidArguments.Count) + { + Log.Error("Too many arguments for command '{CommandName}'", entryName); + 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; + + switch (argumentToMatch.Type) + { + case ConsoleArgumentType.String: + parsedArguments.Add(value); + break; + + case ConsoleArgumentType.Integer when int.TryParse(value, out var intValue): + parsedArguments.Add(intValue); + break; + case ConsoleArgumentType.Integer: + Log.Error("Argument '{Argument}' for command '{CommandName}' is not an integer", value, entryName); + PrintUsage(entry); + return false; + + case ConsoleArgumentType.Float when float.TryParse(value, out var floatValue): + parsedArguments.Add(floatValue); + break; + case ConsoleArgumentType.Float: + Log.Error("Argument '{Argument}' for command '{CommandName}' is not a float", value, entryName); + PrintUsage(entry); + return false; + + case ConsoleArgumentType.Bool when bool.TryParse(value, out var boolValue): + parsedArguments.Add(boolValue); + break; + case ConsoleArgumentType.Bool: + Log.Error("Argument '{Argument}' for command '{CommandName}' is not a boolean", value, entryName); + PrintUsage(entry); + return false; + + default: + 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 (argument.DefaultValue == null) + { + Log.Error("Not enough arguments for command '{CommandName}'", entryName); + PrintUsage(entry); + return false; + } + + parsedArguments.Add(argument.DefaultValue); + } + + if (parsedArguments.Count != entry.ValidArguments.Count) + { + Log.Error("Too many arguments for command '{CommandName}'", entryName); + PrintUsage(entry); + return false; + } + } + } + else + { + if (matches.Count > 1) + { + Log.Error("Command '{CommandName}' does not take any arguments", entryName); + PrintUsage(entry); + return false; + } + } + + 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, + "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; + } + + private static class Traits + { + public static void ThrowIfTIsNullableAndNull(T? argument, [CallerArgumentExpression("argument")] string? paramName = null) + { + if (argument == null && !typeof(T).IsValueType) + throw new ArgumentNullException(paramName); + } + } + + /// + /// Class representing an entry in the console. + /// + private abstract class ConsoleEntry : IConsoleEntry + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the entry. + /// A description of the entry. + public ConsoleEntry(string name, string description) + { + this.Name = name; + this.Description = description; + } + + /// + public string Name { get; } + + /// + 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. + public abstract bool Invoke(IEnumerable arguments); + + /// + /// Get an instance of for a given type. + /// + /// The type of the argument. + /// The default value to use if none is specified. + /// An instance. + /// Thrown if the given type cannot be handled by the console system. + protected static ArgumentInfo TypeToArgument(Type type, object? defaultValue = null) + { + 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); + } + + /// + /// Class representing an alias to another console entry. + /// + private class ConsoleAlias : ConsoleEntry + { + private readonly ConsoleEntry target; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the alias. + /// The target entry to alias to. + public ConsoleAlias(string name, ConsoleEntry target) + : base(name, target.Description) + { + this.target = target; + this.ValidArguments = target.ValidArguments; + } + + /// + public override bool Invoke(IEnumerable arguments) + { + return this.target.Invoke(arguments); + } + } + + /// + /// Class representing a console command. + /// + private class ConsoleCommand : ConsoleEntry, IConsoleCommand + { + private Delegate func; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the variable. + /// A description of the variable. + /// The function to invoke. + public ConsoleCommand(string name, string description, Delegate func) + : 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; + } + + /// + public override bool Invoke(IEnumerable arguments) + { + this.func.DynamicInvoke(arguments.ToArray()); + return true; + } + } + + /// + /// Class representing a basic console variable. + /// + /// The name of the variable. + /// A description of the variable. + private abstract class ConsoleVariable(string name, string description) : ConsoleEntry(name, description); + + /// + /// Class representing a generic console variable. + /// + /// The type of the variable. + private class ConsoleVariable : ConsoleVariable, IConsoleVariable + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the variable. + /// A description of the variable. + public ConsoleVariable(string name, string description) + : base(name, description) + { + this.ValidArguments = new List { TypeToArgument(typeof(T)) }; + } + + /// + public T Value { get; set; } + + /// + public override bool Invoke(IEnumerable arguments) + { + var first = arguments.FirstOrDefault(); + if (first == null || 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; + } + } +} + +/// +/// Exception thrown when a console entry is not found. +/// +internal class EntryNotFoundException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the entry. + public EntryNotFoundException(string name) + : base($"Console entry '{name}' does not exist.") + { + } +} diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 6a419a34a..68d42591d 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Text.RegularExpressions; +using Dalamud.Console; using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; @@ -33,6 +34,9 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag [ServiceManager.ServiceDependency] private readonly ChatGui chatGui = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly ConsoleManager console = Service.Get(); [ServiceManager.ServiceConstructor] private CommandManager(Dalamud dalamud) @@ -47,6 +51,7 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag }; this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled; + this.console.Invoke += this.ConsoleOnInvoke; } /// @@ -132,8 +137,14 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag /// void IInternalDisposableService.DisposeService() { + this.console.Invoke -= this.ConsoleOnInvoke; this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled; } + + private bool ConsoleOnInvoke(string arg) + { + return arg.StartsWith('/') && this.ProcessCommand(arg); + } private void OnCheckMessageHandled(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) { diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 54c5e1ba9..2eb8299b3 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -7,6 +7,7 @@ using System.Runtime.InteropServices; using CheapLoc; using Dalamud.Configuration.Internal; +using Dalamud.Console; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Keys; @@ -101,7 +102,8 @@ internal class DalamudInterface : IInternalDisposableService Game.Framework framework, ClientState clientState, TitleScreenMenu titleScreenMenu, - GameGui gameGui) + GameGui gameGui, + ConsoleManager consoleManager) { this.dalamud = dalamud; this.configuration = configuration; @@ -126,7 +128,8 @@ internal class DalamudInterface : IInternalDisposableService fontAtlasFactory, framework, gameGui, - titleScreenMenu) { IsOpen = false }; + titleScreenMenu, + consoleManager) { IsOpen = false }; this.changelogWindow = new ChangelogWindow( this.titleScreenMenuWindow, fontAtlasFactory, diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 1405e078a..51ab7404a 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -8,6 +8,7 @@ using System.Text; using System.Text.RegularExpressions; using Dalamud.Configuration.Internal; +using Dalamud.Console; using Dalamud.Game; using Dalamud.Game.Command; using Dalamud.Interface.Colors; @@ -90,6 +91,14 @@ internal class ConsoleWindow : Window, IDisposable SerilogEventSink.Instance.LogLine += this.OnLogLine; Service.GetAsync().ContinueWith(r => r.Result.Update += this.FrameworkOnUpdate); + + var cm = Service.Get(); + cm.AddCommand("clear", "Clear the console log", () => + { + this.QueueClear(); + return true; + }); + cm.AddAlias("clear", "cls"); this.Size = new Vector2(500, 400); this.SizeCondition = ImGuiCond.FirstUseEver; @@ -787,11 +796,6 @@ internal class ConsoleWindow : Window, IDisposable { try { - if (this.commandText.StartsWith('/')) - { - this.commandText = this.commandText[1..]; - } - this.historyPos = -1; for (var i = this.history.Count - 1; i >= 0; i--) { @@ -810,7 +814,7 @@ internal class ConsoleWindow : Window, IDisposable return; } - this.lastCmdSuccess = Service.Get().ProcessCommand("/" + this.commandText); + this.lastCmdSuccess = Service.Get().ProcessCommand(this.commandText); this.commandText = string.Empty; // TODO: Force scroll to bottom @@ -839,15 +843,21 @@ internal class ConsoleWindow : Window, IDisposable if (words.Length > 1) return 0; - // TODO: Improve this, add partial completion + // TODO: Improve this, add partial completion, arguments, description, etc. // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L6443-L6484 - var candidates = Service.Get().Commands - .Where(x => x.Key.Contains("/" + words[0])) - .ToList(); - if (candidates.Count > 0) + var candidates = Service.Get().Entries + .Where(x => x.Key.StartsWith(words[0])) + .Select(x => x.Key); + + candidates = candidates.Union( + Service.Get().Commands + .Where(x => x.Key.StartsWith(words[0])).Select(x => x.Key)); + + var enumerable = candidates as string[] ?? candidates.ToArray(); + if (enumerable.Length != 0) { ptr.DeleteChars(0, ptr.BufTextLen); - ptr.InsertChars(0, candidates[0].Key.Replace("/", string.Empty)); + ptr.InsertChars(0, enumerable[0]); } break; diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 0600ec001..00b8d0c39 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Numerics; using Dalamud.Configuration.Internal; +using Dalamud.Console; using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; @@ -41,6 +42,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable private readonly Dictionary shadeEasings = new(); private readonly Dictionary moveEasings = new(); private readonly Dictionary logoEasings = new(); + + private readonly IConsoleVariable showTsm; private InOutCubic? fadeOutEasing; @@ -55,7 +58,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// An instance of . /// An instance of . /// An instance of . - /// An instance of . + /// An instance of . + /// An instance of . public TitleScreenMenuWindow( ClientState clientState, DalamudConfiguration configuration, @@ -63,12 +67,15 @@ internal class TitleScreenMenuWindow : Window, IDisposable FontAtlasFactory fontAtlasFactory, Framework framework, GameGui gameGui, - TitleScreenMenu titleScreenMenu) + TitleScreenMenu titleScreenMenu, + ConsoleManager consoleManager) : base( "TitleScreenMenuOverlay", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus) { + this.showTsm = consoleManager.AddVariable("dalamud.show_tsm", "Show the Title Screen Menu", true); + this.clientState = clientState; this.configuration = configuration; this.gameGui = gameGui; @@ -136,7 +143,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// public override void Draw() { - if (!this.AllowDrawing) + if (!this.AllowDrawing || !this.showTsm.Value) return; var scale = ImGui.GetIO().FontGlobalScale; diff --git a/Dalamud/Logging/Internal/ModuleLog.cs b/Dalamud/Logging/Internal/ModuleLog.cs index 1fe955294..bcbb6e2b1 100644 --- a/Dalamud/Logging/Internal/ModuleLog.cs +++ b/Dalamud/Logging/Internal/ModuleLog.cs @@ -141,8 +141,15 @@ public class ModuleLog public void Fatal(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, exception, values); + /// + /// Log a templated message to the in-game debug log. + /// + /// The log level to log with. + /// The message template to log. + /// The exception to log. + /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - private void WriteLog( + public void WriteLog( LogEventLevel level, string messageTemplate, Exception? exception = null, params object?[] values) { // FIXME: Eventually, the `pluginName` tag should be removed from here and moved over to the actual log From 38eac377a1c066bbda85e17e54abdd786b14444c Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 7 Jun 2024 01:48:17 +0200 Subject: [PATCH 03/16] pi: treat cancellations as errors(fixes #1825) --- .../PluginInstaller/PluginInstallerWindow.cs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index a4efb1f5e..c52e4d635 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -364,10 +364,13 @@ internal class PluginInstallerWindow : Window, IDisposable /// A value indicating whether to continue with the next task. public bool DisplayErrorContinuation(Task task, object state) { - if (task.IsFaulted) - { - var errorModalMessage = state as string; + if (!task.IsFaulted && !task.IsCanceled) + return true; + + var newErrorMessage = state as string; + if (task.Exception != null) + { foreach (var ex in task.Exception.InnerExceptions) { if (ex is PluginException) @@ -375,7 +378,7 @@ internal class PluginInstallerWindow : Window, IDisposable Log.Error(ex, "Plugin installer threw an error"); #if DEBUG if (!string.IsNullOrEmpty(ex.Message)) - errorModalMessage += $"\n\n{ex.Message}"; + newErrorMessage += $"\n\n{ex.Message}"; #endif } else @@ -383,17 +386,15 @@ internal class PluginInstallerWindow : Window, IDisposable Log.Error(ex, "Plugin installer threw an unexpected error"); #if DEBUG if (!string.IsNullOrEmpty(ex.Message)) - errorModalMessage += $"\n\n{ex.Message}"; + newErrorMessage += $"\n\n{ex.Message}"; #endif } } - - this.ShowErrorModal(errorModalMessage); - - return false; } - return true; + this.ShowErrorModal(newErrorMessage ?? "An unknown error occurred."); + + return false; } private void SetOpenPage(PluginInstallerOpenKind kind) From 34d1041a690f5ef6d11bae2f5ec33b13c1d52570 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 7 Jun 2024 19:57:15 +0200 Subject: [PATCH 04/16] pi: log a message if a task was cancelled --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index c52e4d635..a84f7a709 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -391,6 +391,9 @@ internal class PluginInstallerWindow : Window, IDisposable } } } + + if (task.IsCanceled) + Log.Error("A task was cancelled"); this.ShowErrorModal(newErrorMessage ?? "An unknown error occurred."); From 2bee234257e8cdaed7ee6a150cf9b41156a02315 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 7 Jun 2024 19:58:04 +0200 Subject: [PATCH 05/16] console: support removing entries --- Dalamud/Console/ConsoleManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dalamud/Console/ConsoleManager.cs b/Dalamud/Console/ConsoleManager.cs index 17997cc59..a38a9babf 100644 --- a/Dalamud/Console/ConsoleManager.cs +++ b/Dalamud/Console/ConsoleManager.cs @@ -118,6 +118,9 @@ internal partial class ConsoleManager : IServiceType public void RemoveEntry(IConsoleEntry entry) { ArgumentNullException.ThrowIfNull(entry); + + if (!this.entries.Remove(entry.Name)) + throw new EntryNotFoundException(entry.Name); } /// From fabc78b6d41f603dc9e10d3bd6ba0b20bead0edd Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 7 Jun 2024 19:59:17 +0200 Subject: [PATCH 06/16] console: fix up apostrophes while logging --- Dalamud/Console/ConsoleManager.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Dalamud/Console/ConsoleManager.cs b/Dalamud/Console/ConsoleManager.cs index a38a9babf..c4c68e033 100644 --- a/Dalamud/Console/ConsoleManager.cs +++ b/Dalamud/Console/ConsoleManager.cs @@ -200,7 +200,7 @@ internal partial class ConsoleManager : IServiceType var entry = this.FindEntry(entryName); if (entry == null) { - Log.Error("Command '{CommandName}' not found", entryName); + Log.Error("Command {CommandName} not found", entryName); return false; } @@ -212,7 +212,7 @@ internal partial class ConsoleManager : IServiceType { if (i - 1 >= entry.ValidArguments.Count) { - Log.Error("Too many arguments for command '{CommandName}'", entryName); + Log.Error("Too many arguments for command {CommandName}", entryName); PrintUsage(entry); return false; } @@ -237,7 +237,7 @@ internal partial class ConsoleManager : IServiceType parsedArguments.Add(intValue); break; case ConsoleArgumentType.Integer: - Log.Error("Argument '{Argument}' for command '{CommandName}' is not an integer", value, entryName); + Log.Error("Argument {Argument} for command {CommandName} is not an integer", value, entryName); PrintUsage(entry); return false; @@ -245,7 +245,7 @@ internal partial class ConsoleManager : IServiceType parsedArguments.Add(floatValue); break; case ConsoleArgumentType.Float: - Log.Error("Argument '{Argument}' for command '{CommandName}' is not a float", value, entryName); + Log.Error("Argument {Argument} for command {CommandName} is not a float", value, entryName); PrintUsage(entry); return false; @@ -253,7 +253,7 @@ internal partial class ConsoleManager : IServiceType parsedArguments.Add(boolValue); break; case ConsoleArgumentType.Bool: - Log.Error("Argument '{Argument}' for command '{CommandName}' is not a boolean", value, entryName); + Log.Error("Argument {Argument} for command {CommandName} is not a boolean", value, entryName); PrintUsage(entry); return false; @@ -271,7 +271,7 @@ internal partial class ConsoleManager : IServiceType var argument = entry.ValidArguments[i]; if (argument.DefaultValue == null) { - Log.Error("Not enough arguments for command '{CommandName}'", entryName); + Log.Error("Not enough arguments for command {CommandName}", entryName); PrintUsage(entry); return false; } @@ -281,7 +281,7 @@ internal partial class ConsoleManager : IServiceType if (parsedArguments.Count != entry.ValidArguments.Count) { - Log.Error("Too many arguments for command '{CommandName}'", entryName); + Log.Error("Too many arguments for command {CommandName}", entryName); PrintUsage(entry); return false; } @@ -291,7 +291,7 @@ internal partial class ConsoleManager : IServiceType { if (matches.Count > 1) { - Log.Error("Command '{CommandName}' does not take any arguments", entryName); + Log.Error("Command {CommandName} does not take any arguments", entryName); PrintUsage(entry); return false; } From 02022599e5760c0221882c4e58ddd36ea113a866 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 7 Jun 2024 21:04:31 +0200 Subject: [PATCH 07/16] console: add public interface --- Dalamud/Console/ConsoleManager.cs | 4 +- Dalamud/Console/ConsoleManagerPluginScoped.cs | 174 ++++++++++++++++++ Dalamud/Plugin/Services/IConsole.cs | 130 +++++++++++++ 3 files changed, 306 insertions(+), 2 deletions(-) create mode 100644 Dalamud/Console/ConsoleManagerPluginScoped.cs create mode 100644 Dalamud/Plugin/Services/IConsole.cs diff --git a/Dalamud/Console/ConsoleManager.cs b/Dalamud/Console/ConsoleManager.cs index c4c68e033..5ee2b6d72 100644 --- a/Dalamud/Console/ConsoleManager.cs +++ b/Dalamud/Console/ConsoleManager.cs @@ -191,7 +191,7 @@ internal partial class ConsoleManager : IServiceType return false; var entryName = matches[0].Value; - if (string.IsNullOrEmpty(entryName) || entryName.Any(x => x == ' ')) + if (string.IsNullOrEmpty(entryName) || entryName.Any(char.IsWhiteSpace)) { Log.Error("No valid command specified"); return false; @@ -419,7 +419,7 @@ internal partial class ConsoleManager : IServiceType /// private class ConsoleCommand : ConsoleEntry, IConsoleCommand { - private Delegate func; + private readonly Delegate func; /// /// Initializes a new instance of the class. diff --git a/Dalamud/Console/ConsoleManagerPluginScoped.cs b/Dalamud/Console/ConsoleManagerPluginScoped.cs new file mode 100644 index 000000000..3d11aca18 --- /dev/null +++ b/Dalamud/Console/ConsoleManagerPluginScoped.cs @@ -0,0 +1,174 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; + +namespace Dalamud.Console; + +/// +/// Plugin-scoped version of the console service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService +{ + private readonly ConsoleManager console; + + private readonly List trackedEntries = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The plugin this service belongs to. + /// The console manager. + [ServiceManager.ServiceConstructor] + internal ConsoleManagerPluginScoped(LocalPlugin plugin, ConsoleManager console) + { + this.console = console; + + this.Prefix = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(plugin.InternalName); + } + + /// + public string Prefix { get; private set; } + + /// + void IInternalDisposableService.DisposeService() + { + foreach (var trackedEntry in this.trackedEntries) + { + this.console.RemoveEntry(trackedEntry); + } + + this.trackedEntries.Clear(); + } + + /// + public IConsoleCommand AddCommand(string name, string description, Func func) + => this.InternalAddCommand(name, description, func); + + /// + public IConsoleCommand AddCommand(string name, string description, Func func) + => this.InternalAddCommand(name, description, func); + + /// + public IConsoleCommand AddCommand(string name, string description, Func func) + => this.InternalAddCommand(name, description, func); + + /// + public IConsoleCommand AddCommand(string name, string description, Func func) + => this.InternalAddCommand(name, description, func); + + /// + public IConsoleCommand AddCommand(string name, string description, Func func) + => this.InternalAddCommand(name, description, func); + + /// + public IConsoleCommand AddCommand(string name, string description, Func func) + => this.InternalAddCommand(name, description, func); + + /// + public IConsoleVariable AddVariable(string name, string description, T defaultValue) + { + var variable = this.console.AddVariable(this.GetPrefixedName(name), description, defaultValue); + this.trackedEntries.Add(variable); + return variable; + } + + /// + public IConsoleEntry AddAlias(string name, string alias) + { + var entry = this.console.AddAlias(this.GetPrefixedName(name), alias); + this.trackedEntries.Add(entry); + return entry; + } + + /// + public T GetVariable(string name) + { + return this.console.GetVariable(this.GetPrefixedName(name)); + } + + /// + public void SetVariable(string name, T value) + { + this.console.SetVariable(this.GetPrefixedName(name), value); + } + + /// + public void RemoveEntry(IConsoleEntry entry) + { + 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); + this.trackedEntries.Add(command); + return command; + } +} + +/// +/// Utility functions for the console manager. +/// +internal static partial class ConsoleManagerPluginUtil +{ + private static readonly string[] ReservedNamespaces = ["dalamud", "xl"]; + + /// + /// 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/Plugin/Services/IConsole.cs b/Dalamud/Plugin/Services/IConsole.cs new file mode 100644 index 000000000..0b6832efb --- /dev/null +++ b/Dalamud/Plugin/Services/IConsole.cs @@ -0,0 +1,130 @@ +using System.Diagnostics.CodeAnalysis; + +using Dalamud.Console; + +namespace Dalamud.Plugin.Services; + +/// +/// Provides functions to register console commands and variables. +/// +[Experimental("Dalamud001")] +public interface IConsole +{ + /// + /// Gets this plugin's namespace prefix, derived off its internal name. + /// This is the prefix that all commands and variables registered by this plugin will have. + /// If the internal name is "SamplePlugin", the prefix will be "sampleplugin.". + /// + public string Prefix { get; } + + /// + /// Add a command to the console. + /// + /// The name of the command. + /// A description of the command. + /// Function to invoke when the command has been called. Must return a indicating success. + /// The added command. + public IConsoleCommand AddCommand(string name, string description, Func func); + + /// + /// Add a command to the console. + /// + /// The name of the command. + /// A description of the command. + /// Function to invoke when the command has been called. Must return a indicating success. + /// The first argument to the command. + /// The added command. + public IConsoleCommand AddCommand(string name, string description, Func func); + + /// + /// Add a command to the console. + /// + /// The name of the command. + /// A description of the command. + /// Function to invoke when the command has been called. Must return a indicating success. + /// The first argument to the command. + /// The second argument to the command. + /// The added command. + public IConsoleCommand AddCommand(string name, string description, Func func); + + /// + /// Add a command to the console. + /// + /// The name of the command. + /// A description of the command. + /// Function to invoke when the command has been called. Must return a indicating success. + /// The first argument to the command. + /// The second argument to the command. + /// The third argument to the command. + /// The added command. + public IConsoleCommand AddCommand(string name, string description, Func func); + + /// + /// Add a command to the console. + /// + /// The name of the command. + /// A description of the command. + /// Function to invoke when the command has been called. Must return a indicating success. + /// The first argument to the command. + /// The second argument to the command. + /// The third argument to the command. + /// The fourth argument to the command. + /// The added command. + public IConsoleCommand AddCommand( + string name, string description, Func func); + + /// + /// Add a command to the console. + /// + /// The name of the command. + /// A description of the command. + /// Function to invoke when the command has been called. Must return a indicating success. + /// The first argument to the command. + /// The second argument to the command. + /// The third argument to the command. + /// The fourth argument to the command. + /// The fifth argument to the command. + /// The added command. + public IConsoleCommand AddCommand( + string name, string description, Func func); + + /// + /// Add a variable to the console. + /// + /// The name of the variable. + /// A description of the variable. + /// The default value of the variable. + /// The type of the variable. + /// The added variable. + public IConsoleVariable AddVariable(string name, string description, T defaultValue); + + /// + /// Add an alias to a console entry. + /// + /// The name of the entry to add an alias for. + /// The alias to use. + /// The added alias. + public IConsoleEntry AddAlias(string name, string alias); + + /// + /// Get the value of a variable. + /// + /// The name of the variable. + /// The type of the variable. + /// The value of the variable. + public T GetVariable(string name); + + /// + /// Set the value of a variable. + /// + /// The name of the variable. + /// The value to set. + /// The type of the value to set. + public void SetVariable(string name, T value); + + /// + /// Remove an entry from the console. + /// + /// The entry to remove. + public void RemoveEntry(IConsoleEntry entry); +} From 31e541fae595f98a68f056b0e8c04eb8824d1220 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 7 Jun 2024 21:19:16 +0200 Subject: [PATCH 08/16] deps: upgrade StyleCop, fix warnings ...fixes new array initializer expression --- .../Internal/PluginTestingOptIn.cs | 5 +- Dalamud/Console/ConsoleEntry.cs | 2 +- Dalamud/Console/ConsoleManagerPluginScoped.cs | 2 + Dalamud/Dalamud.csproj | 2 +- Dalamud/Game/Addon/Events/AddonEventEntry.cs | 12 +- Dalamud/Game/Config/ConfigChangeEvent.cs | 9 + Dalamud/Game/Config/Properties.cs | 18 + Dalamud/Game/Gui/GameGui.cs | 3 +- .../Structures/InfoProxy/CharacterData.cs | 102 +++--- .../Payloads/AutoTranslatePayload.cs | 20 +- Dalamud/GlobalSuppressions.cs | 2 + Dalamud/Interface/ColorHelpers.cs | 22 +- .../Data/Widgets/NetworkMonitorWidget.cs | 22 +- Dalamud/Interface/Utility/ImVectorWrapper.cs | 320 +++++++++--------- Dalamud/Plugin/InstalledPluginState.cs | 7 + .../Plugin/Internal/Types/PluginPatchData.cs | 26 -- Dalamud/Utility/FuzzyMatcher.cs | 19 +- 17 files changed, 307 insertions(+), 286 deletions(-) delete mode 100644 Dalamud/Plugin/Internal/Types/PluginPatchData.cs diff --git a/Dalamud/Configuration/Internal/PluginTestingOptIn.cs b/Dalamud/Configuration/Internal/PluginTestingOptIn.cs index f40740cf2..30198d563 100644 --- a/Dalamud/Configuration/Internal/PluginTestingOptIn.cs +++ b/Dalamud/Configuration/Internal/PluginTestingOptIn.cs @@ -1,6 +1,9 @@ namespace Dalamud.Configuration.Internal; -public record PluginTestingOptIn +/// +/// Represents a plugin that has opted in to testing. +/// +internal record PluginTestingOptIn { /// /// Initializes a new instance of the class. diff --git a/Dalamud/Console/ConsoleEntry.cs b/Dalamud/Console/ConsoleEntry.cs index 701a4e04b..93f250228 100644 --- a/Dalamud/Console/ConsoleEntry.cs +++ b/Dalamud/Console/ConsoleEntry.cs @@ -34,7 +34,7 @@ public interface IConsoleCommand : IConsoleEntry /// /// Interface representing a variable in the console. /// -/// The type of the variable +/// The type of the variable. public interface IConsoleVariable : IConsoleEntry { /// diff --git a/Dalamud/Console/ConsoleManagerPluginScoped.cs b/Dalamud/Console/ConsoleManagerPluginScoped.cs index 3d11aca18..86949cc74 100644 --- a/Dalamud/Console/ConsoleManagerPluginScoped.cs +++ b/Dalamud/Console/ConsoleManagerPluginScoped.cs @@ -9,6 +9,8 @@ using Dalamud.Plugin.Services; namespace Dalamud.Console; +#pragma warning disable Dalamud001 + /// /// Plugin-scoped version of the console service. /// diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index e8ec785b8..79d3229ab 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -81,7 +81,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dalamud/Game/Addon/Events/AddonEventEntry.cs b/Dalamud/Game/Addon/Events/AddonEventEntry.cs index a7430acf0..bdba9886c 100644 --- a/Dalamud/Game/Addon/Events/AddonEventEntry.cs +++ b/Dalamud/Game/Addon/Events/AddonEventEntry.cs @@ -20,7 +20,7 @@ internal unsafe class AddonEventEntry /// /// Gets the pointer to the addons AtkUnitBase. /// - required public nint Addon { get; init; } + public required nint Addon { get; init; } /// /// Gets the name of the addon this args referrers to. @@ -30,27 +30,27 @@ internal unsafe class AddonEventEntry /// /// Gets the pointer to the event source. /// - required public nint Node { get; init; } + public required nint Node { get; init; } /// /// Gets the handler that gets called when this event is triggered. /// - required public IAddonEventManager.AddonEventHandler Handler { get; init; } + public required IAddonEventManager.AddonEventHandler Handler { get; init; } /// /// Gets the unique id for this event. /// - required public uint ParamKey { get; init; } + public required uint ParamKey { get; init; } /// /// Gets the event type for this event. /// - required public AddonEventType EventType { get; init; } + public required AddonEventType EventType { get; init; } /// /// Gets the event handle for this event. /// - required internal IAddonEventHandle Handle { get; init; } + internal required IAddonEventHandle Handle { get; init; } /// /// Gets the formatted log string for this AddonEventEntry. diff --git a/Dalamud/Game/Config/ConfigChangeEvent.cs b/Dalamud/Game/Config/ConfigChangeEvent.cs index ca898b6b3..8c92e577f 100644 --- a/Dalamud/Game/Config/ConfigChangeEvent.cs +++ b/Dalamud/Game/Config/ConfigChangeEvent.cs @@ -1,5 +1,14 @@ namespace Dalamud.Game.Config; +/// +/// Represents a change in the configuration. +/// +/// The option tha twas changed. public abstract record ConfigChangeEvent(Enum Option); +/// +/// Represents a generic change in the configuration. +/// +/// The option that was changed. +/// The type of the option. public record ConfigChangeEvent(T ConfigOption) : ConfigChangeEvent(ConfigOption) where T : Enum; diff --git a/Dalamud/Game/Config/Properties.cs b/Dalamud/Game/Config/Properties.cs index b43a44a47..4edb17d43 100644 --- a/Dalamud/Game/Config/Properties.cs +++ b/Dalamud/Game/Config/Properties.cs @@ -2,6 +2,24 @@ namespace Dalamud.Game.Config; +/// +/// Represents a string configuration property. +/// +/// The default value. public record StringConfigProperties(SeString? Default); + +/// +/// Represents a uint configuration property. +/// +/// The default value. +/// The minimum value. +/// The maximum value. public record UIntConfigProperties(uint Default, uint Minimum, uint Maximum); + +/// +/// Represents a floating point configuration property. +/// +/// The default value. +/// The minimum value. +/// The maximum value. public record FloatConfigProperties(float Default, float Minimum, float Maximum); diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index bdc483655..d48700af7 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -12,6 +12,7 @@ using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Common.Component.BGCollision; using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; @@ -330,7 +331,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui var index = 0; while (true) { - var agent = agentModule->GetAgentByInternalID((uint)index++); + var agent = agentModule->GetAgentByInternalId((AgentId)index++); if (agent == uiModule || agent == null) break; diff --git a/Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs b/Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs index 0ca35d672..7d3a5d306 100644 --- a/Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs +++ b/Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs @@ -9,6 +9,57 @@ using Lumina.Excel.GeneratedSheets; namespace Dalamud.Game.Network.Structures.InfoProxy; +/// +/// Display group of a character. Used for friends. +/// +public enum DisplayGroup : sbyte +{ + /// + /// All display groups. + /// + All = -1, + + /// + /// No display group. + /// + None, + + /// + /// Star display group. + /// + Star, + + /// + /// Circle display group. + /// + Circle, + + /// + /// Triangle display group. + /// + Triangle, + + /// + /// Diamond display group. + /// + Diamond, + + /// + /// Heart display group. + /// + Heart, + + /// + /// Spade display group. + /// + Spade, + + /// + /// Club display group. + /// + Club, +} + /// /// Dalamud wrapper around a client structs . /// @@ -144,54 +195,3 @@ public unsafe class CharacterData /// internal InfoProxyCommonList.CharacterData* Struct => (InfoProxyCommonList.CharacterData*)this.Address; } - -/// -/// Display group of a character. Used for friends. -/// -public enum DisplayGroup : sbyte -{ - /// - /// All display groups. - /// - All = -1, - - /// - /// No display group. - /// - None, - - /// - /// Star display group. - /// - Star, - - /// - /// Circle display group. - /// - Circle, - - /// - /// Triangle display group. - /// - Triangle, - - /// - /// Diamond display group. - /// - Diamond, - - /// - /// Heart display group. - /// - Heart, - - /// - /// Spade display group. - /// - Spade, - - /// - /// Club display group. - /// - Club, -} diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs index 5c87ad43b..afc1cdc01 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs @@ -13,13 +13,7 @@ namespace Dalamud.Game.Text.SeStringHandling.Payloads; /// public class AutoTranslatePayload : Payload, ITextProvider { - private string text; - - [JsonProperty("group")] - public uint Group { get; private set; } - - [JsonProperty("key")] - public uint Key { get; private set; } + private string? text; /// /// Initializes a new instance of the class. @@ -44,6 +38,18 @@ public class AutoTranslatePayload : Payload, ITextProvider internal AutoTranslatePayload() { } + + /// + /// Gets the autotranslate group. + /// + [JsonProperty("group")] + public uint Group { get; private set; } + + /// + /// Gets the autotranslate key. + /// + [JsonProperty("key")] + public uint Key { get; private set; } /// public override PayloadType Type => PayloadType.AutoTranslateText; diff --git a/Dalamud/GlobalSuppressions.cs b/Dalamud/GlobalSuppressions.cs index 1b869295b..8a9d31b12 100644 --- a/Dalamud/GlobalSuppressions.cs +++ b/Dalamud/GlobalSuppressions.cs @@ -19,6 +19,8 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1117:ParametersMustBeOnSameLineOrSeparateLines", Justification = "I don't care anymore")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1407:ArithmeticExpressionsMustDeclarePrecedence", Justification = "I don't care anymore")] [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1116:SplitParametersMustStartOnLineAfterDeclaration", Justification = "Reviewed.")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "This would be nice, but a big refactor")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileNameMustMatchTypeName", Justification = "I don't like this one so much")] // ImRAII stuff [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")] diff --git a/Dalamud/Interface/ColorHelpers.cs b/Dalamud/Interface/ColorHelpers.cs index 74561f9ef..f88dc7d4f 100644 --- a/Dalamud/Interface/ColorHelpers.cs +++ b/Dalamud/Interface/ColorHelpers.cs @@ -9,17 +9,6 @@ namespace Dalamud.Interface; /// public static class ColorHelpers { - /// - /// A struct representing a color using HSVA coordinates. - /// - /// The hue represented by this struct. - /// The saturation represented by this struct. - /// The value represented by this struct. - /// The alpha represented by this struct. - [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", - Justification = "I don't like it.")] - public record struct HsvaColor(float H, float S, float V, float A); - /// /// Pack a vector4 color into a uint for use in ImGui APIs. /// @@ -305,4 +294,15 @@ public static class ColorHelpers _ => color / 255.0f, }; + + /// + /// A struct representing a color using HSVA coordinates. + /// + /// The hue represented by this struct. + /// The saturation represented by this struct. + /// The value represented by this struct. + /// The alpha represented by this struct. + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", + Justification = "I don't like it.")] + public record struct HsvaColor(float H, float S, float V, float A); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index 41b0904df..69a440713 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -17,17 +17,6 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class NetworkMonitorWidget : IDataWindowWidget { -#pragma warning disable SA1313 - private readonly record struct NetworkPacketData(ushort OpCode, NetworkMessageDirection Direction, uint SourceActorId, uint TargetActorId) -#pragma warning restore SA1313 - { - public readonly IReadOnlyList Data = Array.Empty(); - - public NetworkPacketData(NetworkMonitorWidget widget, ushort opCode, NetworkMessageDirection direction, uint sourceActorId, uint targetActorId, nint dataPtr) - : this(opCode, direction, sourceActorId, targetActorId) - => this.Data = MemoryHelper.Read(dataPtr, widget.GetSizeFromOpCode(opCode), false); - } - private readonly ConcurrentQueue packets = new(); private bool trackNetwork; @@ -214,4 +203,15 @@ internal class NetworkMonitorWidget : IDataWindowWidget /// The filter should find opCodes by number (decimal and hex) and name, if existing. private string OpCodeToString(ushort opCode) => $"{opCode}\0{opCode:X}"; + +#pragma warning disable SA1313 + private readonly record struct NetworkPacketData(ushort OpCode, NetworkMessageDirection Direction, uint SourceActorId, uint TargetActorId) +#pragma warning restore SA1313 + { + public readonly IReadOnlyList Data = Array.Empty(); + + public NetworkPacketData(NetworkMonitorWidget widget, ushort opCode, NetworkMessageDirection direction, uint sourceActorId, uint targetActorId, nint dataPtr) + : this(opCode, direction, sourceActorId, targetActorId) + => this.Data = MemoryHelper.Read(dataPtr, widget.GetSizeFromOpCode(opCode), false); + } } diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.cs b/Dalamud/Interface/Utility/ImVectorWrapper.cs index 5ba1aec2f..f350a6436 100644 --- a/Dalamud/Interface/Utility/ImVectorWrapper.cs +++ b/Dalamud/Interface/Utility/ImVectorWrapper.cs @@ -10,166 +10,6 @@ using JetBrains.Annotations; namespace Dalamud.Interface.Utility; -/// -/// Utility methods for . -/// -public static class ImVectorWrapper -{ - /// - /// Creates a new instance of the struct, initialized with - /// .
- /// You must call after use. - ///
- /// The item type. - /// The initial data. - /// The destroyer function to call on item removal. - /// The minimum capacity of the new vector. - /// The new wrapped vector, that has to be disposed after use. - public static ImVectorWrapper CreateFromEnumerable( - IEnumerable sourceEnumerable, - ImVectorWrapper.ImGuiNativeDestroyDelegate? destroyer = null, - int minCapacity = 0) - where T : unmanaged - { - var res = new ImVectorWrapper(0, destroyer); - try - { - switch (sourceEnumerable) - { - case T[] c: - res.SetCapacity(Math.Max(minCapacity, c.Length + 1)); - res.LengthUnsafe = c.Length; - c.AsSpan().CopyTo(res.DataSpan); - break; - case ICollection c: - res.SetCapacity(Math.Max(minCapacity, c.Count + 1)); - res.AddRange(sourceEnumerable); - break; - case ICollection c: - res.SetCapacity(Math.Max(minCapacity, c.Count + 1)); - res.AddRange(sourceEnumerable); - break; - default: - res.SetCapacity(minCapacity); - res.AddRange(sourceEnumerable); - res.EnsureCapacity(res.LengthUnsafe + 1); - break; - } - - // Null termination - Debug.Assert(res.LengthUnsafe < res.CapacityUnsafe, "Capacity must be more than source length + 1"); - res.StorageSpan[res.LengthUnsafe] = default; - - return res; - } - catch - { - res.Dispose(); - throw; - } - } - - /// - /// Creates a new instance of the struct, initialized with - /// .
- /// You must call after use. - ///
- /// The item type. - /// The initial data. - /// The destroyer function to call on item removal. - /// The minimum capacity of the new vector. - /// The new wrapped vector, that has to be disposed after use. - public static ImVectorWrapper CreateFromSpan( - ReadOnlySpan sourceSpan, - ImVectorWrapper.ImGuiNativeDestroyDelegate? destroyer = null, - int minCapacity = 0) - where T : unmanaged - { - var res = new ImVectorWrapper(Math.Max(minCapacity, sourceSpan.Length + 1), destroyer); - try - { - res.LengthUnsafe = sourceSpan.Length; - sourceSpan.CopyTo(res.DataSpan); - - // Null termination - Debug.Assert(res.LengthUnsafe < res.CapacityUnsafe, "Capacity must be more than source length + 1"); - res.StorageSpan[res.LengthUnsafe] = default; - return res; - } - catch - { - res.Dispose(); - throw; - } - } - - /// - /// Wraps into a .
- /// This does not need to be disposed. - ///
- /// The owner object. - /// The wrapped vector. - public static unsafe ImVectorWrapper ConfigDataWrapped(this ImFontAtlasPtr obj) => - obj.NativePtr is null - ? throw new NullReferenceException() - : new(&obj.NativePtr->ConfigData, ImGuiNative.ImFontConfig_destroy); - - /// - /// Wraps into a .
- /// This does not need to be disposed. - ///
- /// The owner object. - /// The wrapped vector. - public static unsafe ImVectorWrapper FontsWrapped(this ImFontAtlasPtr obj) => - obj.NativePtr is null - ? throw new NullReferenceException() - : new(&obj.NativePtr->Fonts, x => ImGuiNative.ImFont_destroy(x->NativePtr)); - - /// - /// Wraps into a .
- /// This does not need to be disposed. - ///
- /// The owner object. - /// The wrapped vector. - public static unsafe ImVectorWrapper TexturesWrapped(this ImFontAtlasPtr obj) => - obj.NativePtr is null - ? throw new NullReferenceException() - : new(&obj.NativePtr->Textures); - - /// - /// Wraps into a .
- /// This does not need to be disposed. - ///
- /// The owner object. - /// The wrapped vector. - public static unsafe ImVectorWrapper GlyphsWrapped(this ImFontPtr obj) => - obj.NativePtr is null - ? throw new NullReferenceException() - : new(&obj.NativePtr->Glyphs); - - /// - /// Wraps into a .
- /// This does not need to be disposed. - ///
- /// The owner object. - /// The wrapped vector. - public static unsafe ImVectorWrapper IndexedHotDataWrapped(this ImFontPtr obj) - => obj.NativePtr is null - ? throw new NullReferenceException() - : new(&obj.NativePtr->IndexedHotData); - - /// - /// Wraps into a .
- /// This does not need to be disposed. - ///
- /// The owner object. - /// The wrapped vector. - public static unsafe ImVectorWrapper IndexLookupWrapped(this ImFontPtr obj) => - obj.NativePtr is null - ? throw new NullReferenceException() - : new(&obj.NativePtr->IndexLookup); -} - /// /// Wrapper for ImVector. /// @@ -744,3 +584,163 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi private int EnsureIndex(int i) => i >= 0 && i < this.LengthUnsafe ? i : throw new IndexOutOfRangeException(); } + +/// +/// Utility methods for . +/// +public static class ImVectorWrapper +{ + /// + /// Creates a new instance of the struct, initialized with + /// .
+ /// You must call after use. + ///
+ /// The item type. + /// The initial data. + /// The destroyer function to call on item removal. + /// The minimum capacity of the new vector. + /// The new wrapped vector, that has to be disposed after use. + public static ImVectorWrapper CreateFromEnumerable( + IEnumerable sourceEnumerable, + ImVectorWrapper.ImGuiNativeDestroyDelegate? destroyer = null, + int minCapacity = 0) + where T : unmanaged + { + var res = new ImVectorWrapper(0, destroyer); + try + { + switch (sourceEnumerable) + { + case T[] c: + res.SetCapacity(Math.Max(minCapacity, c.Length + 1)); + res.LengthUnsafe = c.Length; + c.AsSpan().CopyTo(res.DataSpan); + break; + case ICollection c: + res.SetCapacity(Math.Max(minCapacity, c.Count + 1)); + res.AddRange(sourceEnumerable); + break; + case ICollection c: + res.SetCapacity(Math.Max(minCapacity, c.Count + 1)); + res.AddRange(sourceEnumerable); + break; + default: + res.SetCapacity(minCapacity); + res.AddRange(sourceEnumerable); + res.EnsureCapacity(res.LengthUnsafe + 1); + break; + } + + // Null termination + Debug.Assert(res.LengthUnsafe < res.CapacityUnsafe, "Capacity must be more than source length + 1"); + res.StorageSpan[res.LengthUnsafe] = default; + + return res; + } + catch + { + res.Dispose(); + throw; + } + } + + /// + /// Creates a new instance of the struct, initialized with + /// .
+ /// You must call after use. + ///
+ /// The item type. + /// The initial data. + /// The destroyer function to call on item removal. + /// The minimum capacity of the new vector. + /// The new wrapped vector, that has to be disposed after use. + public static ImVectorWrapper CreateFromSpan( + ReadOnlySpan sourceSpan, + ImVectorWrapper.ImGuiNativeDestroyDelegate? destroyer = null, + int minCapacity = 0) + where T : unmanaged + { + var res = new ImVectorWrapper(Math.Max(minCapacity, sourceSpan.Length + 1), destroyer); + try + { + res.LengthUnsafe = sourceSpan.Length; + sourceSpan.CopyTo(res.DataSpan); + + // Null termination + Debug.Assert(res.LengthUnsafe < res.CapacityUnsafe, "Capacity must be more than source length + 1"); + res.StorageSpan[res.LengthUnsafe] = default; + return res; + } + catch + { + res.Dispose(); + throw; + } + } + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper ConfigDataWrapped(this ImFontAtlasPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->ConfigData, ImGuiNative.ImFontConfig_destroy); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper FontsWrapped(this ImFontAtlasPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->Fonts, x => ImGuiNative.ImFont_destroy(x->NativePtr)); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper TexturesWrapped(this ImFontAtlasPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->Textures); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper GlyphsWrapped(this ImFontPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->Glyphs); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper IndexedHotDataWrapped(this ImFontPtr obj) + => obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->IndexedHotData); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper IndexLookupWrapped(this ImFontPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->IndexLookup); +} diff --git a/Dalamud/Plugin/InstalledPluginState.cs b/Dalamud/Plugin/InstalledPluginState.cs index 79b9de1ee..3cdd03956 100644 --- a/Dalamud/Plugin/InstalledPluginState.cs +++ b/Dalamud/Plugin/InstalledPluginState.cs @@ -2,5 +2,12 @@ namespace Dalamud.Plugin; +/// +/// State of an installed plugin. +/// +/// The name of the plugin. +/// The internal name of the plugin. +/// Whether or not the plugin is loaded. +/// The version of the plugin. [Api10ToDo("Refactor into an interface, add wrappers for OpenMainUI and OpenConfigUI")] public record InstalledPluginState(string Name, string InternalName, bool IsLoaded, Version Version); diff --git a/Dalamud/Plugin/Internal/Types/PluginPatchData.cs b/Dalamud/Plugin/Internal/Types/PluginPatchData.cs deleted file mode 100644 index f713e4df0..000000000 --- a/Dalamud/Plugin/Internal/Types/PluginPatchData.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.IO; - -namespace Dalamud.Plugin.Internal.Types; - -internal record PluginPatchData -{ - /// - /// Initializes a new instance of the class. - /// - /// DLL file being loaded. - public PluginPatchData(FileSystemInfo dllFile) - { - this.Location = dllFile.FullName; - this.CodeBase = new Uri(dllFile.FullName).AbsoluteUri; - } - - /// - /// Gets simulated Assembly.Location output. - /// - public string Location { get; } - - /// - /// Gets simulated Assembly.CodeBase output. - /// - public string CodeBase { get; } -} diff --git a/Dalamud/Utility/FuzzyMatcher.cs b/Dalamud/Utility/FuzzyMatcher.cs index 9ac71d8bb..03723da89 100644 --- a/Dalamud/Utility/FuzzyMatcher.cs +++ b/Dalamud/Utility/FuzzyMatcher.cs @@ -1,14 +1,20 @@ #define BORDER_MATCHING -namespace Dalamud.Utility; - -using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +namespace Dalamud.Utility; + #pragma warning disable SA1600 #pragma warning disable SA1602 +internal enum MatchMode +{ + Simple, + Fuzzy, + FuzzyParts, +} + internal readonly ref struct FuzzyMatcher { private static readonly (int, int)[] EmptySegArray = Array.Empty<(int, int)>(); @@ -272,12 +278,5 @@ internal readonly ref struct FuzzyMatcher } } -internal enum MatchMode -{ - Simple, - Fuzzy, - FuzzyParts, -} - #pragma warning restore SA1600 #pragma warning restore SA1602 From 8d4e428d6ba85ee800a81d6e8178500bc028b6f3 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 7 Jun 2024 21:26:53 +0200 Subject: [PATCH 09/16] pi: show console prefix for dev plugins --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index a84f7a709..29b0253b8 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using CheapLoc; using Dalamud.Configuration.Internal; +using Dalamud.Console; using Dalamud.Game.Command; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; @@ -2443,6 +2444,7 @@ internal class PluginInstallerWindow : Window, IDisposable { ImGuiHelpers.ScaledDummy(3); ImGui.TextColored(ImGuiColors.DalamudGrey, $"WorkingPluginId: {plugin.EffectiveWorkingPluginId}"); + ImGui.TextColored(ImGuiColors.DalamudGrey, $"Command prefix: {ConsoleManagerPluginUtil.GetSanitizedNamespaceName(plugin.InternalName)}"); ImGuiHelpers.ScaledDummy(3); } From b3727a0f2ae5b47e0635af054fa0b0cd3248d31b Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 7 Jun 2024 21:27:23 +0200 Subject: [PATCH 10/16] console: also strip "plugin" from namespace --- Dalamud/Console/ConsoleManagerPluginScoped.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Console/ConsoleManagerPluginScoped.cs b/Dalamud/Console/ConsoleManagerPluginScoped.cs index 86949cc74..fed614cfc 100644 --- a/Dalamud/Console/ConsoleManagerPluginScoped.cs +++ b/Dalamud/Console/ConsoleManagerPluginScoped.cs @@ -139,7 +139,7 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService ///
internal static partial class ConsoleManagerPluginUtil { - private static readonly string[] ReservedNamespaces = ["dalamud", "xl"]; + private static readonly string[] ReservedNamespaces = ["dalamud", "xl", "plugin"]; /// /// Get a sanitized namespace name from a plugin's internal name. From 0f2aaa4241820748d4a4e30dd7c8e34972e2135a Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 7 Jun 2024 21:53:29 +0200 Subject: [PATCH 11/16] console: add "toggle" command to flip boolean convars --- Dalamud/Console/ConsoleManager.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Dalamud/Console/ConsoleManager.cs b/Dalamud/Console/ConsoleManager.cs index 5ee2b6d72..445bc3cd1 100644 --- a/Dalamud/Console/ConsoleManager.cs +++ b/Dalamud/Console/ConsoleManager.cs @@ -27,6 +27,7 @@ internal partial class ConsoleManager : IServiceType [ServiceManager.ServiceConstructor] public ConsoleManager() { + this.AddCommand("toggle", "Toggle a boolean variable.", this.OnToggleVariable); } /// @@ -317,6 +318,19 @@ internal partial class ConsoleManager : IServiceType { return this.entries.TryGetValue(name, out var entry) ? entry as ConsoleEntry : null; } + + private bool OnToggleVariable(string name) + { + if (this.FindEntry(name) is not IConsoleVariable variable) + { + Log.Error("Variable {VariableName} not found or not a boolean", name); + return false; + } + + variable.Value = !variable.Value; + + return true; + } private static class Traits { From c0867d887ef6fc5d3f3785f4aaea23f1ef175bd2 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 7 Jun 2024 22:52:33 +0200 Subject: [PATCH 12/16] console: correctly treat null default values, return result from commands --- Dalamud/Console/ConsoleManager.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dalamud/Console/ConsoleManager.cs b/Dalamud/Console/ConsoleManager.cs index 445bc3cd1..6600069c2 100644 --- a/Dalamud/Console/ConsoleManager.cs +++ b/Dalamud/Console/ConsoleManager.cs @@ -384,6 +384,9 @@ internal partial class ConsoleManager : IServiceType /// Thrown if the given type cannot be handled by the console system. protected static ArgumentInfo TypeToArgument(Type type, object? defaultValue = null) { + // If the default value is DBNull, we want to treat it as null + defaultValue = defaultValue == DBNull.Value ? null : defaultValue; + if (type == typeof(string)) return new ArgumentInfo(ConsoleArgumentType.String, defaultValue); @@ -462,8 +465,7 @@ internal partial class ConsoleManager : IServiceType /// public override bool Invoke(IEnumerable arguments) { - this.func.DynamicInvoke(arguments.ToArray()); - return true; + return (bool)this.func.DynamicInvoke(arguments.ToArray())!; } } From 830207f43e5eec3713c3e98c5cdc69d12a41c125 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 8 Jun 2024 20:40:02 +0200 Subject: [PATCH 13/16] pm: consider plugins without a valid source repo as orphaned --- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 227a6c707..84ce753cc 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -178,7 +178,6 @@ internal class LocalPlugin : IDisposable /// Gets a value indicating whether or not this plugin is orphaned(belongs to a repo) or not. /// public bool IsOrphaned => !this.IsDev && - !this.manifest.InstalledFromUrl.IsNullOrEmpty() && // TODO(api8): Remove this, all plugins will have a proper flag this.GetSourceRepository() == null; /// From 625a71a6d1f672ce2aae2949481ed78927b706cf Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 8 Jun 2024 21:43:48 +0200 Subject: [PATCH 14/16] remove inaccurate information from manifest comment --- Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs index 31469a914..dc05409b0 100644 --- a/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs @@ -36,7 +36,7 @@ internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest /// /// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party - /// repo. Unless the manifest has been manually modified, this is determined by the InstalledFromUrl being null. + /// repo. /// public bool IsThirdParty => !this.InstalledFromUrl.IsNullOrEmpty() && this.InstalledFromUrl != SpecialPluginSource.MainRepo; From cf4ecacf850ed5cd5c40cc5971e0b8357b0a7c55 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 8 Jun 2024 21:44:25 +0200 Subject: [PATCH 15/16] pm: make sure that interface and scope are always disposed if ctor throws This fixes an issue wherein dev plugins could end up in a half-disposed state if the ctor were to throw an exception --- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 56 +++++++++++++------- 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 84ce753cc..36af7e4bc 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -412,28 +412,39 @@ internal class LocalPlugin : IDisposable this.ServiceScope = ioc.GetScope(); this.ServiceScope.RegisterPrivateScopes(this); // Add this LocalPlugin as a private scope, so services can get it - if (this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1) + try { - this.instance = await framework.RunOnFrameworkThread( - () => this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!)) as IDalamudPlugin; + if (this.manifest.LoadSync && this.manifest.LoadRequiredState is 0 or 1) + { + this.instance = await framework.RunOnFrameworkThread( + () => this.ServiceScope.CreateAsync( + this.pluginType!, + this.DalamudInterface!)) as IDalamudPlugin; + } + else + { + this.instance = + await this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin; + } } - else + catch (Exception ex) { - this.instance = - await this.ServiceScope.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin; + Log.Error(ex, "Exception in plugin constructor"); + this.instance = null; } if (this.instance == null) { this.State = PluginState.LoadError; - this.DalamudInterface.DisposeInternal(); + this.UnloadAndDisposeState(); + Log.Error( - $"Error while loading {this.Name}, failed to bind and call the plugin constructor"); + "Error while loading {PluginName}, failed to bind and call the plugin constructor", this.InternalName); return; } this.State = PluginState.Loaded; - Log.Information($"Finished loading {this.DllFile.Name}"); + Log.Information("Finished loading {PluginName}", this.InternalName); } catch (Exception ex) { @@ -443,7 +454,7 @@ internal class LocalPlugin : IDisposable if (ex is PluginPreconditionFailedException) Log.Warning(ex.Message); else - Log.Error(ex, $"Error while loading {this.Name}"); + Log.Error(ex, "Error while loading {PluginName}", this.InternalName); throw; } @@ -498,15 +509,7 @@ internal class LocalPlugin : IDisposable await framework.RunOnFrameworkThread(() => this.instance?.Dispose()); this.instance = null; - - this.DalamudInterface?.DisposeInternal(); - this.DalamudInterface = null; - - this.ServiceScope?.Dispose(); - this.ServiceScope = null; - - this.pluginType = null; - this.pluginAssembly = null; + this.UnloadAndDisposeState(); if (!reloading) { @@ -681,4 +684,19 @@ internal class LocalPlugin : IDisposable throw new InvalidPluginException(this.DllFile); } } + + private void UnloadAndDisposeState() + { + if (this.instance != null) + throw new InvalidOperationException("Plugin instance should be disposed at this point"); + + this.DalamudInterface?.DisposeInternal(); + this.DalamudInterface = null; + + this.ServiceScope?.Dispose(); + this.ServiceScope = null; + + this.pluginType = null; + this.pluginAssembly = null; + } } From 1d939e2587768037d43a2807a16ffc854e5bdcd3 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 9 Jun 2024 01:32:20 +0200 Subject: [PATCH 16/16] build: 9.1.0.10 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 79d3229ab..5cf16101f 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.1.0.9 + 9.1.0.10 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion)