feat: Use Debug Command Handler for Dalamud Commands (#2018)

* feat: new command handler that works off a hook

* cr comment

* Use ClientStructs for sig
This commit is contained in:
KazWolfe 2024-08-28 13:23:39 -07:00 committed by GitHub
parent 9de58b0cb9
commit 0fb7585973
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 30 additions and 57 deletions

View file

@ -2,56 +2,45 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Console; using Dalamud.Console;
using Dalamud.Game.Gui; using Dalamud.Hooking;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.Shell;
namespace Dalamud.Game.Command; namespace Dalamud.Game.Command;
/// <summary> /// <summary>
/// This class manages registered in-game slash commands. /// This class manages registered in-game slash commands.
/// </summary> /// </summary>
[ServiceManager.EarlyLoadedService] [ServiceManager.EarlyLoadedService]
internal sealed class CommandManager : IInternalDisposableService, ICommandManager internal sealed unsafe class CommandManager : IInternalDisposableService, ICommandManager
{ {
private static readonly ModuleLog Log = new("Command"); private static readonly ModuleLog Log = new("Command");
private readonly ConcurrentDictionary<string, IReadOnlyCommandInfo> commandMap = new(); private readonly ConcurrentDictionary<string, IReadOnlyCommandInfo> commandMap = new();
private readonly ConcurrentDictionary<(string, IReadOnlyCommandInfo), string> commandAssemblyNameMap = new(); private readonly ConcurrentDictionary<(string, IReadOnlyCommandInfo), string> commandAssemblyNameMap = new();
private readonly Regex commandRegexEn = new(@"^The command (?<command>.+) does not exist\.$", RegexOptions.Compiled);
private readonly Regex commandRegexJp = new(@"^そのコマンドはありません。: (?<command>.+)$", RegexOptions.Compiled);
private readonly Regex commandRegexDe = new(@"^„(?<command>.+)“ existiert nicht als Textkommando\.$", RegexOptions.Compiled);
private readonly Regex commandRegexFr = new(@"^La commande texte “(?<command>.+)” n'existe pas\.$", RegexOptions.Compiled);
private readonly Regex commandRegexCn = new(@"^^(“|「)(?<command>.+)(”|」)(出现问题:该命令不存在|出現問題:該命令不存在)。$", RegexOptions.Compiled);
private readonly Regex currentLangCommandRegex;
[ServiceManager.ServiceDependency] private readonly Hook<ShellCommands.Delegates.TryInvokeDebugCommand>? tryInvokeDebugCommandHook;
private readonly ChatGui chatGui = Service<ChatGui>.Get();
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly ConsoleManager console = Service<ConsoleManager>.Get(); private readonly ConsoleManager console = Service<ConsoleManager>.Get();
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private CommandManager(Dalamud dalamud) private CommandManager(Dalamud dalamud)
{ {
this.currentLangCommandRegex = (ClientLanguage)dalamud.StartInfo.Language switch this.tryInvokeDebugCommandHook = Hook<ShellCommands.Delegates.TryInvokeDebugCommand>.FromAddress(
{ (nint)ShellCommands.MemberFunctionPointers.TryInvokeDebugCommand,
ClientLanguage.Japanese => this.commandRegexJp, this.OnTryInvokeDebugCommand);
ClientLanguage.English => this.commandRegexEn, this.tryInvokeDebugCommandHook.Enable();
ClientLanguage.German => this.commandRegexDe,
ClientLanguage.French => this.commandRegexFr,
_ => this.commandRegexEn,
};
this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled;
this.console.Invoke += this.ConsoleOnInvoke; 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); Log.Error(ex, "Error while dispatching command {CommandName} (Argument: {Argument})", command, argument);
} }
} }
/// <summary> /// <summary>
/// Add a command handler, which you can use to add your own custom commands to the in-game chat. /// Add a command handler, which you can use to add your own custom commands to the in-game chat.
/// </summary> /// </summary>
@ -131,7 +120,7 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
Log.Error("Command {CommandName} is already registered", command); Log.Error("Command {CommandName} is already registered", command);
return false; return false;
} }
if (!this.commandAssemblyNameMap.TryAdd((command, info), loaderAssemblyName)) if (!this.commandAssemblyNameMap.TryAdd((command, info), loaderAssemblyName))
{ {
this.commandMap.Remove(command, out _); this.commandMap.Remove(command, out _);
@ -184,7 +173,8 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
/// </summary> /// </summary>
/// <param name="assemblyName">The name of the assembly.</param> /// <param name="assemblyName">The name of the assembly.</param>
/// <returns>A list of commands and their associated activation string.</returns> /// <returns>A list of commands and their associated activation string.</returns>
public List<KeyValuePair<(string Command, IReadOnlyCommandInfo CommandInfo), string>> GetHandlersByAssemblyName(string assemblyName) public List<KeyValuePair<(string Command, IReadOnlyCommandInfo CommandInfo), string>> GetHandlersByAssemblyName(
string assemblyName)
{ {
return this.commandAssemblyNameMap.Where(c => c.Value == assemblyName).ToList(); return this.commandAssemblyNameMap.Where(c => c.Value == assemblyName).ToList();
} }
@ -193,37 +183,20 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.console.Invoke -= this.ConsoleOnInvoke; this.console.Invoke -= this.ConsoleOnInvoke;
this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled; this.tryInvokeDebugCommandHook?.Dispose();
} }
private bool ConsoleOnInvoke(string arg) private bool ConsoleOnInvoke(string arg)
{ {
return arg.StartsWith('/') && this.ProcessCommand(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 result = this.tryInvokeDebugCommandHook!.OriginalDisposeSafe(self, command, uiModule);
{ if (result != -1) return result;
var cmdMatch = this.currentLangCommandRegex.Match(message.TextValue).Groups["command"];
if (cmdMatch.Success) return this.ProcessCommand(command->ToString()) ? 0 : result;
{
// 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;
}
}
}
} }
} }
@ -238,7 +211,7 @@ internal sealed class CommandManager : IInternalDisposableService, ICommandManag
internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager
{ {
private static readonly ModuleLog Log = new("Command"); private static readonly ModuleLog Log = new("Command");
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly CommandManager commandManagerService = Service<CommandManager>.Get(); private readonly CommandManager commandManagerService = Service<CommandManager>.Get();
@ -253,10 +226,10 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
{ {
this.pluginInfo = localPlugin; this.pluginInfo = localPlugin;
} }
/// <inheritdoc/> /// <inheritdoc/>
public ReadOnlyDictionary<string, IReadOnlyCommandInfo> Commands => this.commandManagerService.Commands; public ReadOnlyDictionary<string, IReadOnlyCommandInfo> Commands => this.commandManagerService.Commands;
/// <inheritdoc/> /// <inheritdoc/>
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
@ -264,7 +237,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
{ {
this.commandManagerService.RemoveHandler(command); this.commandManagerService.RemoveHandler(command);
} }
this.pluginRegisteredCommands.Clear(); this.pluginRegisteredCommands.Clear();
} }
@ -275,7 +248,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
/// <inheritdoc/> /// <inheritdoc/>
public void DispatchCommand(string command, string argument, IReadOnlyCommandInfo info) public void DispatchCommand(string command, string argument, IReadOnlyCommandInfo info)
=> this.commandManagerService.DispatchCommand(command, argument, info); => this.commandManagerService.DispatchCommand(command, argument, info);
/// <inheritdoc/> /// <inheritdoc/>
public bool AddHandler(string command, CommandInfo info) public bool AddHandler(string command, CommandInfo info)
{ {
@ -294,7 +267,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand
return false; return false;
} }
/// <inheritdoc/> /// <inheritdoc/>
public bool RemoveHandler(string command) public bool RemoveHandler(string command)
{ {

View file

@ -158,7 +158,7 @@ public static class Util
var asm = typeof(Util).Assembly; var asm = typeof(Util).Assembly;
var attrs = asm.GetCustomAttributes<AssemblyMetadataAttribute>(); var attrs = asm.GetCustomAttributes<AssemblyMetadataAttribute>();
return gitHashInternal = attrs.First(a => a.Key == "GitHash").Value; return gitHashInternal = attrs.FirstOrDefault(a => a.Key == "GitHash")?.Value ?? "N/A";
} }
/// <summary> /// <summary>