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/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..93f250228 --- /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..6600069c2 --- /dev/null +++ b/Dalamud/Console/ConsoleManager.cs @@ -0,0 +1,526 @@ +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() + { + 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. + /// + /// 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); + + if (!this.entries.Remove(entry.Name)) + throw new EntryNotFoundException(entry.Name); + } + + /// + /// 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(char.IsWhiteSpace)) + { + 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 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 + { + 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 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); + + 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 readonly 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) + { + return (bool)this.func.DynamicInvoke(arguments.ToArray())!; + } + } + + /// + /// 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/Console/ConsoleManagerPluginScoped.cs b/Dalamud/Console/ConsoleManagerPluginScoped.cs new file mode 100644 index 000000000..fed614cfc --- /dev/null +++ b/Dalamud/Console/ConsoleManagerPluginScoped.cs @@ -0,0 +1,176 @@ +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; + +#pragma warning disable Dalamud001 + +/// +/// 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", "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.csproj b/Dalamud/Dalamud.csproj index 1286f089e..d60c8285b 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) @@ -82,7 +82,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 cf160bf6c..b127d2b8e 100644 --- a/Dalamud/Game/Addon/Events/AddonEventEntry.cs +++ b/Dalamud/Game/Addon/Events/AddonEventEntry.cs @@ -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/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 571c5b385..10d27a527 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, int timestamp, ref SeString sender, ref SeString message, ref bool isHandled) { 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/Network/Structures/InfoProxy/CharacterData.cs b/Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs index 25952db35..96de1c3b2 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 971e546bc..fe4c19329 100644 --- a/Dalamud/Interface/ColorHelpers.cs +++ b/Dalamud/Interface/ColorHelpers.cs @@ -10,17 +10,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. /// @@ -316,4 +305,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/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) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 06795aa53..f9b675112 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 83d49456e..12c3a2960 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; @@ -89,6 +90,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; @@ -786,11 +795,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--) { @@ -809,7 +813,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 @@ -838,15 +842,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/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/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index e404f805c..0d65a2873 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; @@ -365,10 +366,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) @@ -376,7 +380,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 @@ -384,17 +388,18 @@ 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; } + + if (task.IsCanceled) + Log.Error("A task was cancelled"); - return true; + this.ShowErrorModal(newErrorMessage ?? "An unknown error occurred."); + + return false; } private void SetOpenPage(PluginInstallerOpenKind kind) @@ -2472,6 +2477,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); } 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/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/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 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/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 6c3ca8c0c..7d9b79e9b 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; /// @@ -413,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) { @@ -444,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; } @@ -499,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) { @@ -676,4 +678,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; + } } 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; 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/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); +} 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