From 44ae4dbc4908248f94e16015af0b0af08667ff1f Mon Sep 17 00:00:00 2001 From: regenerance <161264320+regenerance@users.noreply.github.com> Date: Fri, 18 Sep 2026 12:26:00 -0700 Subject: [PATCH 1/3] t4-replacefunc --- src/game/t4/mp/components/events.cpp | 2 + src/game/t4/mp/components/gsc_functions.cpp | 154 ++++++++++++++++++++ src/game/t4/mp/components/gsc_functions.h | 1 + src/game/t4/mp/structs.h | 32 ++++ 4 files changed, 189 insertions(+) diff --git a/src/game/t4/mp/components/events.cpp b/src/game/t4/mp/components/events.cpp index 34a49e4a..0ff1d782 100644 --- a/src/game/t4/mp/components/events.cpp +++ b/src/game/t4/mp/components/events.cpp @@ -64,6 +64,8 @@ void *Events::Scr_ShutdownSystem_Hook(scriptInstance_t inst, int sys, int bCompl vmShutdownHandlers[i](); } + GSCFunctions::ClearReplacedFunctions(); + return Scr_ShutdownSystem_Detour.GetOriginal()(inst, sys, bComplete); } diff --git a/src/game/t4/mp/components/gsc_functions.cpp b/src/game/t4/mp/components/gsc_functions.cpp index dbb30ffc..944380c1 100644 --- a/src/game/t4/mp/components/gsc_functions.cpp +++ b/src/game/t4/mp/components/gsc_functions.cpp @@ -3,11 +3,13 @@ #include "common/gsc_registry.h" #include "common/script_files.h" #include "sv_bots.h" +#include namespace t4 { namespace mp { +static std::unordered_map ReplacedFunctions; static void CloseAllScriptFiles() { script_files::CloseAll(); @@ -172,8 +174,156 @@ void GSCrGetPlayerclipBrushesContainingPoint() } } +static const char* GetCodePosForParam(int index) +{ + const scriptInstance_t inst = SCRIPTINSTANCE_SERVER; + const int numParams = static_cast(Scr_GetNumParam(inst)); + + if (index < 0 || index >= numParams) + return nullptr; + + auto* top = *reinterpret_cast(0x85B11094 + (static_cast(inst) * 0x14)); + + if (!top) + return nullptr; + + auto* value = top - index; + + if (value->type != VAR_FUNCTION) + return nullptr; + + return value->u.codePosValue; +} + +static const char* GSC_ResolveReplaceFunc(const char* pos) +{ + if (!pos) + return nullptr; + + const auto it = ReplacedFunctions.find(pos); + + if (it == ReplacedFunctions.end()) + return nullptr; + + printf( + "[ReplaceFunc HIT] %p -> %p | count=%u\n", + pos, + it->second, + static_cast(ReplacedFunctions.size()) + ); + + return it->second; +} + +extern "C" const char* T4_GSC_GetReplacedPos(const char* pos) +{ + const char* replacement = GSC_ResolveReplaceFunc(pos); + + if (replacement) + { + printf("[ReplaceFunc VM HIT] %p -> %p\n", pos, replacement); + return replacement; + } + + return pos; +} + +static void GScr_ReplaceFunc() +{ + if (Scr_GetNumParam(SCRIPTINSTANCE_SERVER) != 2) + { + Scr_Error( + "replaceFunc: needs two parameters", + SCRIPTINSTANCE_SERVER + ); + return; + } + + const char* what = GetCodePosForParam(0); + const char* with = GetCodePosForParam(1); + + if (!what) + { + Scr_Error( + "replaceFunc: first parameter is not a function", + SCRIPTINSTANCE_SERVER + ); + return; + } + + if (!with) + { + Scr_Error( + "replaceFunc: second parameter is not a function", + SCRIPTINSTANCE_SERVER + ); + return; + } + + printf( + "[ReplaceFunc REGISTER] %p -> %p | before=%u\n", + what, + with, + static_cast(ReplacedFunctions.size()) + ); + + ReplacedFunctions[what] = with; + + printf( + "[ReplaceFunc REGISTER] after=%u\n", + static_cast(ReplacedFunctions.size()) + ); +} + +Detour VMExecuteInternal_Detour; +extern "C" __declspec(naked) void VM_Execute_Hook() +{ + __asm + { + mr r3, r10 + bl T4_GSC_GetReplacedPos + + mr r10, r3 + + addi r11, r10, 1 + lbz r9, 0(r10) + addi r10, r27, -0x4068 + slwi r21, r31, 2 + + lis r12, 0x8234 + ori r12, r12, 0x6480 + mtctr r12 + bctr + } +} + +void GSCFunctions::ClearReplacedFunctions() +{ + printf( + "[ReplaceFunc CLEAR] before=%u\n", + static_cast(ReplacedFunctions.size()) + ); + + for (auto it = ReplacedFunctions.begin(); it != ReplacedFunctions.end(); ++it) + { + printf( + "[ReplaceFunc CLEAR] removing %p -> %p\n", + it->first, + it->second + ); + } + + ReplacedFunctions.clear(); + + printf( + "[ReplaceFunc CLEAR] after=%u\n", + static_cast(ReplacedFunctions.size()) + ); +} + static const gsc::Entry functions[] = { {"addtestclient", GScr_AddTestClient, BUILTIN_ANY}, + {"replaceFunc", GScr_ReplaceFunc, BUILTIN_ANY}, {"getplayerclipbrushescontainingpoint", GSCrGetPlayerclipBrushesContainingPoint, BUILTIN_ANY}, {"fs_testfile", GScr_FS_TestFile, BUILTIN_ANY}, {"fs_fopen", GScr_FS_FOpen, BUILTIN_ANY}, @@ -200,11 +350,15 @@ GSCFunctions::GSCFunctions() { Scr_GetFunction_Detour = Detour(Scr_GetFunction, Scr_GetFunction_Hook); Scr_GetFunction_Detour.Install(); + + VMExecuteInternal_Detour = Detour(reinterpret_cast(0x82346470), reinterpret_cast(VM_Execute_Hook)); + VMExecuteInternal_Detour.Install(); } GSCFunctions::~GSCFunctions() { Scr_GetFunction_Detour.Remove(); + VMExecuteInternal_Detour.Remove(); CloseAllScriptFiles(); } } // namespace mp diff --git a/src/game/t4/mp/components/gsc_functions.h b/src/game/t4/mp/components/gsc_functions.h index 97f396f7..cc2890d2 100644 --- a/src/game/t4/mp/components/gsc_functions.h +++ b/src/game/t4/mp/components/gsc_functions.h @@ -13,6 +13,7 @@ class GSCFunctions : public Module ~GSCFunctions(); static void OnVMShutdown(); + static void ClearReplacedFunctions(); }; } // namespace mp } // namespace t4 diff --git a/src/game/t4/mp/structs.h b/src/game/t4/mp/structs.h index 6da9e6b9..0d519b2d 100644 --- a/src/game/t4/mp/structs.h +++ b/src/game/t4/mp/structs.h @@ -1393,5 +1393,37 @@ enum connstate_t CA_ACTIVE = 0x9, }; +enum Vartype_t : __int32 +{ + VAR_UNDEFINED = 0x0, + VAR_POINTER = 0x1, + VAR_STRING = 0x2, + VAR_ISTRING = 0x3, + VAR_VECTOR = 0x4, + VAR_FLOAT = 0x5, + VAR_INTEGER = 0x6, + VAR_CODEPOS = 0x7, + VAR_PRECODEPOS = 0x8, + VAR_FUNCTION = 0x9, +}; + +union VariableUnion +{ + int intValue; + unsigned int uintValue; + float floatValue; + const char* stringValue; + const char* codePosValue; + unsigned int pointerValue; +}; + +struct VariableValue +{ + VariableUnion u; + Vartype_t type; +}; + +static_assert(sizeof(VariableValue) == 0x8, ""); + } // namespace mp } // namespace t4 From 0ba52eab8f8aefc1846dc9ecaa5b3e7730b67abb Mon Sep 17 00:00:00 2001 From: regenerance <161264320+regenerance@users.noreply.github.com> Date: Fri, 18 Sep 2026 21:33:46 -0700 Subject: [PATCH 2/3] fixed clearing function --- src/game/t4/mp/components/events.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/game/t4/mp/components/events.cpp b/src/game/t4/mp/components/events.cpp index 0ff1d782..dbecd5b5 100644 --- a/src/game/t4/mp/components/events.cpp +++ b/src/game/t4/mp/components/events.cpp @@ -64,7 +64,10 @@ void *Events::Scr_ShutdownSystem_Hook(scriptInstance_t inst, int sys, int bCompl vmShutdownHandlers[i](); } - GSCFunctions::ClearReplacedFunctions(); + if (bComplete) + { + GSCFunctions::ClearReplacedFunctions(); + } return Scr_ShutdownSystem_Detour.GetOriginal()(inst, sys, bComplete); } From f79b72c6bb40e6938f33239c9f640fbef531a81c Mon Sep 17 00:00:00 2001 From: Michael Oliver Date: Tue, 6 Oct 2026 14:01:55 +0100 Subject: [PATCH 3/3] refactor: replacefunc VM state and shutdown handling --- src/game/t4/mp/components/events.cpp | 13 +- src/game/t4/mp/components/gsc_functions.cpp | 149 ++++++-------------- src/game/t4/mp/components/gsc_functions.h | 17 ++- src/game/t4/mp/structs.h | 47 +++++- src/game/t4/mp/symbols.h | 1 + 5 files changed, 111 insertions(+), 116 deletions(-) diff --git a/src/game/t4/mp/components/events.cpp b/src/game/t4/mp/components/events.cpp index dbecd5b5..5ae4a602 100644 --- a/src/game/t4/mp/components/events.cpp +++ b/src/game/t4/mp/components/events.cpp @@ -59,14 +59,13 @@ Detour Events::Cmd_Init_Detour; void *Events::Scr_ShutdownSystem_Hook(scriptInstance_t inst, int sys, int bComplete) { - for (size_t i = 0; i < ARRAYSIZE(vmShutdownHandlers); ++i) + // These handlers own server script state. + if (inst == SCRIPTINSTANCE_SERVER) { - vmShutdownHandlers[i](); - } - - if (bComplete) - { - GSCFunctions::ClearReplacedFunctions(); + for (size_t i = 0; i < ARRAYSIZE(vmShutdownHandlers); ++i) + { + vmShutdownHandlers[i](); + } } return Scr_ShutdownSystem_Detour.GetOriginal()(inst, sys, bComplete); diff --git a/src/game/t4/mp/components/gsc_functions.cpp b/src/game/t4/mp/components/gsc_functions.cpp index 944380c1..0dee2768 100644 --- a/src/game/t4/mp/components/gsc_functions.cpp +++ b/src/game/t4/mp/components/gsc_functions.cpp @@ -3,13 +3,12 @@ #include "common/gsc_registry.h" #include "common/script_files.h" #include "sv_bots.h" -#include namespace t4 { namespace mp { -static std::unordered_map ReplacedFunctions; +std::unordered_map GSCFunctions::ReplacedFunctions; static void CloseAllScriptFiles() { script_files::CloseAll(); @@ -17,6 +16,7 @@ static void CloseAllScriptFiles() void GSCFunctions::OnVMShutdown() { + ReplacedFunctions.clear(); CloseAllScriptFiles(); } @@ -174,20 +174,16 @@ void GSCrGetPlayerclipBrushesContainingPoint() } } -static const char* GetCodePosForParam(int index) +Detour GSCFunctions::VM_Execute_Detour; + +const char *GSCFunctions::GetCodePosForParam(int index) { - const scriptInstance_t inst = SCRIPTINSTANCE_SERVER; - const int numParams = static_cast(Scr_GetNumParam(inst)); + const int numParams = static_cast(Scr_GetNumParam(SCRIPTINSTANCE_SERVER)); if (index < 0 || index >= numParams) return nullptr; - auto* top = *reinterpret_cast(0x85B11094 + (static_cast(inst) * 0x14)); - - if (!top) - return nullptr; - - auto* value = top - index; + const VariableValue *value = &scrVmPub[SCRIPTINSTANCE_SERVER].top[-index]; if (value->type != VAR_FUNCTION) return nullptr; @@ -195,93 +191,27 @@ static const char* GetCodePosForParam(int index) return value->u.codePosValue; } -static const char* GSC_ResolveReplaceFunc(const char* pos) +const char *GSCFunctions::GetReplacedPos(const char *pos, scriptInstance_t inst) { - if (!pos) - return nullptr; + if (inst != SCRIPTINSTANCE_SERVER || !pos || ReplacedFunctions.empty()) + return pos; const auto it = ReplacedFunctions.find(pos); - - if (it == ReplacedFunctions.end()) - return nullptr; - - printf( - "[ReplaceFunc HIT] %p -> %p | count=%u\n", - pos, - it->second, - static_cast(ReplacedFunctions.size()) - ); - - return it->second; + return it != ReplacedFunctions.end() ? it->second : pos; } -extern "C" const char* T4_GSC_GetReplacedPos(const char* pos) +extern "C" const char *T4_GetReplacedPos(const char *pos, scriptInstance_t inst) { - const char* replacement = GSC_ResolveReplaceFunc(pos); - - if (replacement) - { - printf("[ReplaceFunc VM HIT] %p -> %p\n", pos, replacement); - return replacement; - } - - return pos; -} - -static void GScr_ReplaceFunc() -{ - if (Scr_GetNumParam(SCRIPTINSTANCE_SERVER) != 2) - { - Scr_Error( - "replaceFunc: needs two parameters", - SCRIPTINSTANCE_SERVER - ); - return; - } - - const char* what = GetCodePosForParam(0); - const char* with = GetCodePosForParam(1); - - if (!what) - { - Scr_Error( - "replaceFunc: first parameter is not a function", - SCRIPTINSTANCE_SERVER - ); - return; - } - - if (!with) - { - Scr_Error( - "replaceFunc: second parameter is not a function", - SCRIPTINSTANCE_SERVER - ); - return; - } - - printf( - "[ReplaceFunc REGISTER] %p -> %p | before=%u\n", - what, - with, - static_cast(ReplacedFunctions.size()) - ); - - ReplacedFunctions[what] = with; - - printf( - "[ReplaceFunc REGISTER] after=%u\n", - static_cast(ReplacedFunctions.size()) - ); + return GSCFunctions::GetReplacedPos(pos, inst); } -Detour VMExecuteInternal_Detour; -extern "C" __declspec(naked) void VM_Execute_Hook() +__declspec(naked) void GSCFunctions::VM_Execute_Hook() { __asm { mr r3, r10 - bl T4_GSC_GetReplacedPos + mr r4, r31 + bl T4_GetReplacedPos mr r10, r3 @@ -297,33 +227,35 @@ extern "C" __declspec(naked) void VM_Execute_Hook() } } -void GSCFunctions::ClearReplacedFunctions() +void GSCFunctions::ReplaceFunc() { - printf( - "[ReplaceFunc CLEAR] before=%u\n", - static_cast(ReplacedFunctions.size()) - ); + if (Scr_GetNumParam(SCRIPTINSTANCE_SERVER) != 2) + { + Scr_Error("replacefunc: expected two function parameters", SCRIPTINSTANCE_SERVER); + return; + } + + const char *what = GetCodePosForParam(0); + const char *with = GetCodePosForParam(1); - for (auto it = ReplacedFunctions.begin(); it != ReplacedFunctions.end(); ++it) + if (!what) { - printf( - "[ReplaceFunc CLEAR] removing %p -> %p\n", - it->first, - it->second - ); + Scr_Error("replacefunc: first parameter must be a function", SCRIPTINSTANCE_SERVER); + return; } - ReplacedFunctions.clear(); + if (!with) + { + Scr_Error("replacefunc: second parameter must be a function", SCRIPTINSTANCE_SERVER); + return; + } - printf( - "[ReplaceFunc CLEAR] after=%u\n", - static_cast(ReplacedFunctions.size()) - ); + ReplacedFunctions[what] = with; } static const gsc::Entry functions[] = { {"addtestclient", GScr_AddTestClient, BUILTIN_ANY}, - {"replaceFunc", GScr_ReplaceFunc, BUILTIN_ANY}, + {"replacefunc", GSCFunctions::ReplaceFunc, BUILTIN_ANY}, {"getplayerclipbrushescontainingpoint", GSCrGetPlayerclipBrushesContainingPoint, BUILTIN_ANY}, {"fs_testfile", GScr_FS_TestFile, BUILTIN_ANY}, {"fs_fopen", GScr_FS_FOpen, BUILTIN_ANY}, @@ -341,7 +273,10 @@ BuiltinFunction Scr_GetFunction_Hook(const char **pName, int *type) { const gsc::Entry *function = gsc::Find(*pName, functions); if (function) + { + *type = function->type; return function->actionFunc; + } } return Scr_GetFunction_Detour.GetOriginal()(pName, type); } @@ -351,14 +286,16 @@ GSCFunctions::GSCFunctions() Scr_GetFunction_Detour = Detour(Scr_GetFunction, Scr_GetFunction_Hook); Scr_GetFunction_Detour.Install(); - VMExecuteInternal_Detour = Detour(reinterpret_cast(0x82346470), reinterpret_cast(VM_Execute_Hook)); - VMExecuteInternal_Detour.Install(); + // Intercept TU7 VM_Execute's opcode fetch, then resume at 0x82346480. + VM_Execute_Detour = Detour(reinterpret_cast(0x82346470), reinterpret_cast(VM_Execute_Hook)); + VM_Execute_Detour.Install(); } GSCFunctions::~GSCFunctions() { Scr_GetFunction_Detour.Remove(); - VMExecuteInternal_Detour.Remove(); + VM_Execute_Detour.Remove(); + ReplacedFunctions.clear(); CloseAllScriptFiles(); } } // namespace mp diff --git a/src/game/t4/mp/components/gsc_functions.h b/src/game/t4/mp/components/gsc_functions.h index cc2890d2..c259e11c 100644 --- a/src/game/t4/mp/components/gsc_functions.h +++ b/src/game/t4/mp/components/gsc_functions.h @@ -1,11 +1,15 @@ #pragma once #include "pch.h" +#include namespace t4 { namespace mp { +// Keep the assembly bridge distinct from IW3's C-linkage GetReplacedPos symbol. +extern "C" const char *T4_GetReplacedPos(const char *pos, scriptInstance_t inst); + class GSCFunctions : public Module { public: @@ -13,7 +17,18 @@ class GSCFunctions : public Module ~GSCFunctions(); static void OnVMShutdown(); - static void ClearReplacedFunctions(); + static void ReplaceFunc(); + + private: + friend const char *T4_GetReplacedPos(const char *pos, scriptInstance_t inst); + + static std::unordered_map ReplacedFunctions; + + static const char *GetCodePosForParam(int index); + static const char *GetReplacedPos(const char *pos, scriptInstance_t inst); + + static Detour VM_Execute_Detour; + static void VM_Execute_Hook(); }; } // namespace mp } // namespace t4 diff --git a/src/game/t4/mp/structs.h b/src/game/t4/mp/structs.h index 0d519b2d..c1f60e25 100644 --- a/src/game/t4/mp/structs.h +++ b/src/game/t4/mp/structs.h @@ -1407,14 +1407,19 @@ enum Vartype_t : __int32 VAR_FUNCTION = 0x9, }; +struct VariableStackBuffer; + union VariableUnion { int intValue; unsigned int uintValue; float floatValue; - const char* stringValue; - const char* codePosValue; + unsigned int stringValue; + const float *vectorValue; + const char *codePosValue; unsigned int pointerValue; + VariableStackBuffer *stackValue; + unsigned int entityOffset; }; struct VariableValue @@ -1425,5 +1430,43 @@ struct VariableValue static_assert(sizeof(VariableValue) == 0x8, ""); +struct function_stack_t +{ + const char *pos; + unsigned int localId; + unsigned int localVarCount; + VariableValue *top; + VariableValue *startTop; +}; + +struct function_frame_t +{ + function_stack_t fs; + int topType; +}; + +struct scrVmPub_t +{ + unsigned int *localVars; + VariableValue *maxstack; + int function_count; + function_frame_t *function_frame; + VariableValue *top; + bool debugCode; + bool abort_on_error; + bool terminal_error; + unsigned int inparamcount; + unsigned int outparamcount; + function_frame_t function_frame_start[32]; + VariableValue stack[2048]; +}; + +static_assert(sizeof(function_frame_t) == 0x18, ""); +static_assert(offsetof(scrVmPub_t, top) == 0x10, ""); +static_assert(offsetof(scrVmPub_t, outparamcount) == 0x1C, ""); +static_assert(offsetof(scrVmPub_t, function_frame_start) == 0x20, ""); +static_assert(offsetof(scrVmPub_t, stack) == 0x320, ""); +static_assert(sizeof(scrVmPub_t) == 0x4320, ""); + } // namespace mp } // namespace t4 diff --git a/src/game/t4/mp/symbols.h b/src/game/t4/mp/symbols.h index 8f33db97..91c16f38 100644 --- a/src/game/t4/mp/symbols.h +++ b/src/game/t4/mp/symbols.h @@ -174,6 +174,7 @@ static auto Scr_GetGenericField = reinterpret_cast(0x8234AFD0); static auto Scr_GetNumParam = reinterpret_cast(0x82345650); +static scrVmPub_t *scrVmPub = reinterpret_cast(0x85AF2F98); static auto Scr_GetObjectField = reinterpret_cast(0x822556B0); static auto Scr_GetString = reinterpret_cast(0x8234B550);