From 0fb7585973ffe22ab9353d853b0084a3f1c0803b Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Wed, 28 Aug 2024 13:23:39 -0700 Subject: [PATCH] feat: Use Debug Command Handler for Dalamud Commands (#2018) * feat: new command handler that works off a hook * cr comment * Use ClientStructs for sig --- Dalamud/Game/Command/CommandManager.cs | 85 +++++++++----------------- Dalamud/Utility/Util.cs | 2 +- 2 files changed, 30 insertions(+), 57 deletions(-) diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index aa6798171..078ce8c50 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -2,56 +2,45 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Text.RegularExpressions; using Dalamud.Console; -using Dalamud.Game.Gui; -using Dalamud.Game.Text; -using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.Shell; + namespace Dalamud.Game.Command; /// /// This class manages registered in-game slash commands. /// [ServiceManager.EarlyLoadedService] -internal sealed class CommandManager : IInternalDisposableService, ICommandManager +internal sealed unsafe class CommandManager : IInternalDisposableService, ICommandManager { private static readonly ModuleLog Log = new("Command"); private readonly ConcurrentDictionary commandMap = new(); private readonly ConcurrentDictionary<(string, IReadOnlyCommandInfo), string> commandAssemblyNameMap = new(); - private readonly Regex commandRegexEn = new(@"^The command (?.+) does not exist\.$", RegexOptions.Compiled); - private readonly Regex commandRegexJp = new(@"^そのコマンドはありません。: (?.+)$", RegexOptions.Compiled); - private readonly Regex commandRegexDe = new(@"^„(?.+)“ existiert nicht als Textkommando\.$", RegexOptions.Compiled); - private readonly Regex commandRegexFr = new(@"^La commande texte “(?.+)” n'existe pas\.$", RegexOptions.Compiled); - private readonly Regex commandRegexCn = new(@"^^(“|「)(?.+)(”|」)(出现问题:该命令不存在|出現問題:該命令不存在)。$", RegexOptions.Compiled); - private readonly Regex currentLangCommandRegex; - [ServiceManager.ServiceDependency] - private readonly ChatGui chatGui = Service.Get(); - + private readonly Hook? tryInvokeDebugCommandHook; + [ServiceManager.ServiceDependency] private readonly ConsoleManager console = Service.Get(); [ServiceManager.ServiceConstructor] private CommandManager(Dalamud dalamud) { - this.currentLangCommandRegex = (ClientLanguage)dalamud.StartInfo.Language switch - { - ClientLanguage.Japanese => this.commandRegexJp, - ClientLanguage.English => this.commandRegexEn, - ClientLanguage.German => this.commandRegexDe, - ClientLanguage.French => this.commandRegexFr, - _ => this.commandRegexEn, - }; + this.tryInvokeDebugCommandHook = Hook.FromAddress( + (nint)ShellCommands.MemberFunctionPointers.TryInvokeDebugCommand, + this.OnTryInvokeDebugCommand); + this.tryInvokeDebugCommandHook.Enable(); - this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled; this.console.Invoke += this.ConsoleOnInvoke; } @@ -113,7 +102,7 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag Log.Error(ex, "Error while dispatching command {CommandName} (Argument: {Argument})", command, argument); } } - + /// /// Add a command handler, which you can use to add your own custom commands to the in-game chat. /// @@ -131,7 +120,7 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag Log.Error("Command {CommandName} is already registered", command); return false; } - + if (!this.commandAssemblyNameMap.TryAdd((command, info), loaderAssemblyName)) { this.commandMap.Remove(command, out _); @@ -184,7 +173,8 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag /// /// The name of the assembly. /// A list of commands and their associated activation string. - public List> GetHandlersByAssemblyName(string assemblyName) + public List> GetHandlersByAssemblyName( + string assemblyName) { return this.commandAssemblyNameMap.Where(c => c.Value == assemblyName).ToList(); } @@ -193,37 +183,20 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag void IInternalDisposableService.DisposeService() { this.console.Invoke -= this.ConsoleOnInvoke; - this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled; + this.tryInvokeDebugCommandHook?.Dispose(); } - + 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) + private int OnTryInvokeDebugCommand(ShellCommands* self, Utf8String* command, UIModule* uiModule) { - if (type == XivChatType.ErrorMessage && timestamp == 0) - { - var cmdMatch = this.currentLangCommandRegex.Match(message.TextValue).Groups["command"]; - if (cmdMatch.Success) - { - // Yes, it's a chat command. - var command = cmdMatch.Value; - if (this.ProcessCommand(command)) isHandled = true; - } - else - { - // Always match for china, since they patch in language files without changing the ClientLanguage. - cmdMatch = this.commandRegexCn.Match(message.TextValue).Groups["command"]; - if (cmdMatch.Success) - { - // Yes, it's a Chinese fallback chat command. - var command = cmdMatch.Value; - if (this.ProcessCommand(command)) isHandled = true; - } - } - } + var result = this.tryInvokeDebugCommandHook!.OriginalDisposeSafe(self, command, uiModule); + if (result != -1) return result; + + return this.ProcessCommand(command->ToString()) ? 0 : result; } } @@ -238,7 +211,7 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager { private static readonly ModuleLog Log = new("Command"); - + [ServiceManager.ServiceDependency] private readonly CommandManager commandManagerService = Service.Get(); @@ -253,10 +226,10 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand { this.pluginInfo = localPlugin; } - + /// public ReadOnlyDictionary Commands => this.commandManagerService.Commands; - + /// void IInternalDisposableService.DisposeService() { @@ -264,7 +237,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand { this.commandManagerService.RemoveHandler(command); } - + this.pluginRegisteredCommands.Clear(); } @@ -275,7 +248,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand /// public void DispatchCommand(string command, string argument, IReadOnlyCommandInfo info) => this.commandManagerService.DispatchCommand(command, argument, info); - + /// public bool AddHandler(string command, CommandInfo info) { @@ -294,7 +267,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand return false; } - + /// public bool RemoveHandler(string command) { diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 7d7bb1380..d8e05716e 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -158,7 +158,7 @@ public static class Util var asm = typeof(Util).Assembly; var attrs = asm.GetCustomAttributes(); - return gitHashInternal = attrs.First(a => a.Key == "GitHash").Value; + return gitHashInternal = attrs.FirstOrDefault(a => a.Key == "GitHash")?.Value ?? "N/A"; } ///