From c225b058623dc4c1a9adc0e713351bb967709203 Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Wed, 22 Oct 2025 17:30:44 +0800 Subject: [PATCH 1/3] feat: Implement OriginalArgs getter for IOnCommandExecuteHookEvent --- .../EventParams/OnCommandExecuteHookEvent.cs | 1 + .../Services/CoreHookService.cs | 17 ++++++++++--- .../EventParams/IOnCommandExecuteHookEvent.cs | 5 ++++ managed/src/TestPlugin/TestPlugin.cs | 2 +- plugin_files/gamedata/cs2/core/offsets.jsonc | 24 +++++++++++++++++++ 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnCommandExecuteHookEvent.cs b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnCommandExecuteHookEvent.cs index 981a1cfd..2ea5ccf2 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnCommandExecuteHookEvent.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Events/EventParams/OnCommandExecuteHookEvent.cs @@ -6,6 +6,7 @@ namespace SwiftlyS2.Core.Events; internal class OnCommandExecuteHookEvent : IOnCommandExecuteHookEvent { public required string OriginalName { get; init; } + public required string[] OriginalArgs { get; init; } public string CommandName { get; set; } = string.Empty; public required HookMode HookMode { get; init; } diff --git a/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs b/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs index 926ce0ef..8df7443e 100644 --- a/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs +++ b/managed/src/SwiftlyS2.Core/Services/CoreHookService.cs @@ -104,22 +104,32 @@ private void HookCommandExecute() _Logger.LogInformation("Hooking Cmd_ExecuteCommand at {Address}", address); var commandNameOffset = NativeOffsets.Fetch("CommandNameOffset"); + var commandArgsOffset = NativeOffsets.Fetch("CommandArgsOffset"); _ExecuteCommand = _Core.Memory.GetUnmanagedFunctionByAddress(address); _ExecuteCommandGuid = _ExecuteCommand.AddHook((next) => { return (a1, a2, a3, a4, a5) => { - var (commandName, commandPtr) = (a5 != nint.Zero && a5 < nint.MaxValue && commandNameOffset != 0) switch + var commandName = (a5 != nint.Zero && a5 < nint.MaxValue && commandNameOffset != 0) switch { true when Marshal.ReadIntPtr(new nint(a5 + commandNameOffset)) is var basePtr && basePtr != nint.Zero && basePtr < nint.MaxValue - => (Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(basePtr)) ?? string.Empty, Marshal.ReadIntPtr(basePtr)), - _ => (string.Empty, nint.Zero) + => Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(basePtr)) ?? string.Empty, + _ => string.Empty }; + var commandArgs = (a5 != nint.Zero && a5 < nint.MaxValue && commandArgsOffset != 0) switch + { + true => Marshal.PtrToStringAnsi(new nint(a5 + commandArgsOffset)) ?? string.Empty, + _ => string.Empty + }; + + var argsSplit = commandArgs.Split(' ', StringSplitOptions.RemoveEmptyEntries); + var args = argsSplit.Length > 1 ? argsSplit[1..] : []; var preEvent = new OnCommandExecuteHookEvent { OriginalName = commandName, + OriginalArgs = args, HookMode = HookMode.Pre }; EventPublisher.InvokeOnCommandExecuteHook(preEvent); @@ -142,6 +152,7 @@ true when Marshal.ReadIntPtr(new nint(a5 + commandNameOffset)) is var basePtr && var postEvent = new OnCommandExecuteHookEvent { OriginalName = commandName, + OriginalArgs = args, HookMode = HookMode.Post }; EventPublisher.InvokeOnCommandExecuteHook(postEvent); diff --git a/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnCommandExecuteHookEvent.cs b/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnCommandExecuteHookEvent.cs index 3ad311a2..699f9300 100644 --- a/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnCommandExecuteHookEvent.cs +++ b/managed/src/SwiftlyS2.Shared/Modules/Events/EventParams/IOnCommandExecuteHookEvent.cs @@ -12,6 +12,11 @@ public interface IOnCommandExecuteHookEvent { /// public string OriginalName { get; } + /// + /// The original command arguments. + /// + public string[] OriginalArgs { get; } + /// /// The command arguments. /// diff --git a/managed/src/TestPlugin/TestPlugin.cs b/managed/src/TestPlugin/TestPlugin.cs index 3afd3499..80f69e5e 100644 --- a/managed/src/TestPlugin/TestPlugin.cs +++ b/managed/src/TestPlugin/TestPlugin.cs @@ -77,7 +77,7 @@ public override void Load(bool hotReload) // Core.Event.OnCommandExecuteHook += (@event) => // { - // Console.WriteLine($"[TestPlugin] CommandExecute({@event.HookMode}): {@event.OriginalName}"); + // Console.WriteLine($"[TestPlugin] CommandExecute({@event.HookMode}): {@event.OriginalName} ({string.Join(" ", @event.OriginalArgs)})"); // @event.SetCommandName("test"); // }; Core.Engine.ExecuteCommandWithBuffer("@ping", (buffer) => diff --git a/plugin_files/gamedata/cs2/core/offsets.jsonc b/plugin_files/gamedata/cs2/core/offsets.jsonc index 669ae0b5..674410ee 100644 --- a/plugin_files/gamedata/cs2/core/offsets.jsonc +++ b/plugin_files/gamedata/cs2/core/offsets.jsonc @@ -97,6 +97,30 @@ "windows": 1088, "linux": 1088 }, + /* + To locate this offset, first refer to my ExecuteCommandDelegate comments in CoreHookService.cs. + The function prologue disassembly: + + .text:00000000001C34E0 mov [rsp-8+arg_8], rbx + .text:00000000001C34E5 mov [rsp-8+arg_18], r9 + .text:00000000001C34EA mov [rsp-8+arg_0], rcx + ... + .text:00000000001C3509 mov r14, [rbp+0D00h+arg_20] + + Following the Windows x64 calling convention, parameters a1-a4 are passed via rcx, rdx, r8, r9 respectively, + while a5 (the first stack parameter) is loaded into r14 from [rbp+0D00h+arg_20]. + + By setting a breakpoint at the mov instruction above and executing an invalid command in the CS2 server console, + we can inspect the memory region pointed to by r14. + The command string with arguments appears at offset 0x20 from the base address in r14. + + Note: Unlike CommandNameOffset (which points to null-byte ('\0') delimited tokens), + CommandArgsOffset points to a space-delimited string containing the full command line. + */ + "CommandArgsOffset": { + "windows": 32, + "linux": 32 + }, /* From sdk */ "ICvar::FindConCommand": { "windows": 17, From cbd971d1799fed50ee6353c9cb9f0cad2a18611d Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Wed, 22 Oct 2025 17:44:11 +0800 Subject: [PATCH 2/3] chore: Update TestPlugin.cs --- managed/src/TestPlugin/TestPlugin.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/managed/src/TestPlugin/TestPlugin.cs b/managed/src/TestPlugin/TestPlugin.cs index 80f69e5e..0e602dbf 100644 --- a/managed/src/TestPlugin/TestPlugin.cs +++ b/managed/src/TestPlugin/TestPlugin.cs @@ -75,11 +75,12 @@ public override void Load(bool hotReload) // Console.WriteLine($"[TestPlugin] ConsoleOutput: {@event.Message}"); // }; - // Core.Event.OnCommandExecuteHook += (@event) => - // { - // Console.WriteLine($"[TestPlugin] CommandExecute({@event.HookMode}): {@event.OriginalName} ({string.Join(" ", @event.OriginalArgs)})"); - // @event.SetCommandName("test"); - // }; + Core.Event.OnCommandExecuteHook += (@event) => + { + if (@event.HookMode == HookMode.Pre) return; + Core.Logger.LogInformation("CommandExecute: {name} with {args}", @event.OriginalName, @event.OriginalArgs.Length > 0 ? string.Join(" ", @event.OriginalArgs) : "no args"); + // @event.SetCommandName("test"); + }; Core.Engine.ExecuteCommandWithBuffer("@ping", (buffer) => { Console.WriteLine($"pong: {buffer}"); From 9bae3c5f9ad764806c4f157f354fd43a33c3165c Mon Sep 17 00:00:00 2001 From: Ambr0se Date: Thu, 23 Oct 2025 00:29:43 +0800 Subject: [PATCH 3/3] chore: Cleanup code --- managed/src/TestPlugin/TestPlugin.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/managed/src/TestPlugin/TestPlugin.cs b/managed/src/TestPlugin/TestPlugin.cs index 0e602dbf..c9ff4404 100644 --- a/managed/src/TestPlugin/TestPlugin.cs +++ b/managed/src/TestPlugin/TestPlugin.cs @@ -75,12 +75,12 @@ public override void Load(bool hotReload) // Console.WriteLine($"[TestPlugin] ConsoleOutput: {@event.Message}"); // }; - Core.Event.OnCommandExecuteHook += (@event) => - { - if (@event.HookMode == HookMode.Pre) return; - Core.Logger.LogInformation("CommandExecute: {name} with {args}", @event.OriginalName, @event.OriginalArgs.Length > 0 ? string.Join(" ", @event.OriginalArgs) : "no args"); - // @event.SetCommandName("test"); - }; + // Core.Event.OnCommandExecuteHook += (@event) => + // { + // if (@event.HookMode == HookMode.Pre) return; + // Core.Logger.LogInformation("CommandExecute: {name} with {args}", @event.OriginalName, @event.OriginalArgs.Length > 0 ? string.Join(" ", @event.OriginalArgs) : "no args"); + // // @event.SetCommandName("test"); + // }; Core.Engine.ExecuteCommandWithBuffer("@ping", (buffer) => { Console.WriteLine($"pong: {buffer}");