From 1d720425109f15ce11e818465ad1bdeaa51a5e2e Mon Sep 17 00:00:00 2001 From: Betsruner <55611319+betsruner@users.noreply.github.com> Date: Thu, 30 Apr 2026 02:07:05 -0500 Subject: [PATCH 1/3] Added runtime vscript checksum recording --- src/Modules/EngineDemoPlayer.cpp | 1 + src/Modules/EngineDemoRecorder.cpp | 15 ++++++ src/Modules/EngineDemoRecorder.hpp | 3 ++ src/Modules/Server.cpp | 79 ++++++++++++++++++++++++++++++ src/Offsets/Portal 2 9568.hpp | 1 + 5 files changed, 99 insertions(+) diff --git a/src/Modules/EngineDemoPlayer.cpp b/src/Modules/EngineDemoPlayer.cpp index 86987edeb..5da1f24fd 100644 --- a/src/Modules/EngineDemoPlayer.cpp +++ b/src/Modules/EngineDemoPlayer.cpp @@ -174,6 +174,7 @@ std::string EngineDemoPlayer::GetLevelName() { // 0x11: VPK internal checksums // 0x12: incomplete speedrun summary // 0x13: speedrun identifier +// 0x14: runtime vscript checksum void EngineDemoPlayer::CustomDemoData(char *data, size_t length) { if (data[0] == 0x03 || data[0] == 0x04) { // Entity input data std::optional slot; diff --git a/src/Modules/EngineDemoRecorder.cpp b/src/Modules/EngineDemoRecorder.cpp index 249f64cd6..987c63bbd 100644 --- a/src/Modules/EngineDemoRecorder.cpp +++ b/src/Modules/EngineDemoRecorder.cpp @@ -103,6 +103,20 @@ static void RecordQueuedCommands() { engine->demorecorder->queuedCommands.clear(); } +static void RecordQueuedVScriptChecksums() { + for (auto &queuedChecksum : engine->demorecorder->queuedVScriptChecksums) { + size_t nameLen = queuedChecksum.first.size(); + size_t bufLen = nameLen + 6; + auto *buf = new uint8_t[bufLen]; + buf[0] = 0x14; + *reinterpret_cast(buf + 1) = queuedChecksum.second; + strcpy(reinterpret_cast(buf + 5), queuedChecksum.first.c_str()); + engine->demorecorder->RecordData(buf, bufLen); + delete[] buf; + } + engine->demorecorder->queuedVScriptChecksums.clear(); +} + ON_EVENT(SESSION_END) { if (*engine->demorecorder->m_bRecording && sar_autorecord.GetInt() == -1) { engine->demorecorder->Stop(); @@ -172,6 +186,7 @@ DETOUR(EngineDemoRecorder::SetSignonState, int state) { RecordTimestamp(); SpeedrunTimer::WriteIdToDemo(); // Write speedrun ID to every demo segment RecordQueuedCommands(); + RecordQueuedVScriptChecksums(); SpeedrunTimer::RecordIncompleteSummary(); engine->ExecuteCommand("echo \"SAR " SAR_VERSION " (Built " SAR_BUILT ")\"", true); AddDemoFileChecksums(); diff --git a/src/Modules/EngineDemoRecorder.hpp b/src/Modules/EngineDemoRecorder.hpp index bc82b09c2..80007056f 100644 --- a/src/Modules/EngineDemoRecorder.hpp +++ b/src/Modules/EngineDemoRecorder.hpp @@ -5,6 +5,8 @@ #include "Utils.hpp" #include +#include +#include // Ticks before demo autostop #define DEMO_AUTOSTOP_DELAY 15 @@ -28,6 +30,7 @@ class EngineDemoRecorder : public Module { int autorecordStartNum = 1; std::vector queuedCommands = {}; + std::vector> queuedVScriptChecksums = {}; char coopRadialMenuLastPos[8]; diff --git a/src/Modules/Server.cpp b/src/Modules/Server.cpp index b687276a1..a96f1b6a2 100644 --- a/src/Modules/Server.cpp +++ b/src/Modules/Server.cpp @@ -41,6 +41,7 @@ #include "Offsets.hpp" #include "Utils.hpp" #include "Variable.hpp" +#include "Utils/lodepng.hpp" #include "Features/OverlayRender.hpp" @@ -336,6 +337,70 @@ static bool FindClosestPassableSpace_Detour(void *entity, const Vector &ind_push Hook FindClosestPassableSpace_Hook(&FindClosestPassableSpace_Detour); static int (*UTIL_GetCommandClientIndex)(); +static constexpr uint8_t SAR_MSG_VSCRIPT_RUNTIME_CHECKSUM = 0x14; + +static size_t BoundedCStringLen(const char *str, size_t maxLen) { + if (!str) return 0; + size_t len = 0; + while (len < maxLen && str[len]) ++len; + return len; +} + +static void RecordRuntimeVscriptChecksum(const char *scriptName, const char *scriptData) { + if (!scriptName || !*scriptName || !scriptData || !*scriptData) return; + + // Meant to avoid an unbounded C-String search in case a file data ever isn't null-terminated + constexpr size_t MAX_SCRIPT_SIZE = 8 * 1024 * 1024; + size_t scriptLen = BoundedCStringLen(scriptData, MAX_SCRIPT_SIZE); + if (scriptLen == 0) return; + + uint32_t sum = 0; + if (scriptLen < MAX_SCRIPT_SIZE) { + sum = lodepng_crc32(reinterpret_cast(scriptData), scriptLen); + } + + if (engine->demorecorder->isRecordingDemo) { + size_t nameLen = strlen(scriptName); + size_t bufLen = nameLen + 6; + auto *buf = new uint8_t[bufLen]; + buf[0] = SAR_MSG_VSCRIPT_RUNTIME_CHECKSUM; + *reinterpret_cast(buf + 1) = sum; + strcpy(reinterpret_cast(buf + 5), scriptName); + engine->demorecorder->RecordData(buf, bufLen); + delete[] buf; + } else { + engine->demorecorder->queuedVScriptChecksums.emplace_back(scriptName, sum); + } +} + +#ifdef _WIN32 +using _VScript_CompileScript = int(__rescall *)(void *thisptr, const char *scriptData, const char *scriptName); +#else +using _VScript_CompileScript = int(__cdecl *)(void *thisptr, const char *scriptData, const char *scriptName); +#endif +static _VScript_CompileScript VScript_CompileScript; +extern Hook g_VScriptCompileScriptHook; +#ifdef _WIN32 +static int __fastcall VScript_CompileScript_Hook(void *thisptr, int edx, const char *scriptData, const char *scriptName) +#else +static int __cdecl VScript_CompileScript_Hook(void *thisptr, const char *scriptData, const char *scriptName) +#endif +{ +#ifdef _WIN32 + (void)edx; +#endif + (void)thisptr; + + if (scriptName && *scriptName) { + RecordRuntimeVscriptChecksum(scriptName, scriptData); + } + + g_VScriptCompileScriptHook.Disable(); + auto ret = VScript_CompileScript(thisptr, scriptData, scriptName); + g_VScriptCompileScriptHook.Enable(); + return ret; +} +Hook g_VScriptCompileScriptHook(&VScript_CompileScript_Hook); extern Hook g_ViewPunch_Hook; DETOUR_T(void, Server::ViewPunch, const QAngle &offset) { @@ -969,6 +1034,20 @@ bool Server::Init() { if (sar.game->Is(SourceGame_Portal2 | SourceGame_Portal2_2011)) { Server::IsInPVS = (Server::_IsInPVS)Memory::Scan(this->Name(), Offsets::IsInPVS); g_IsInPVS_Hook.SetFunc(IsInPVS); + +#ifdef _WIN32 + const char *vscriptModuleName = "vscript.dll"; +#else + const char *vscriptModuleName = "vscript.so"; +#endif + + auto vscriptCompileScript = Memory::Absolute(vscriptModuleName, Offsets::VScript_CompileScript); + if (vscriptCompileScript) { + VScript_CompileScript = reinterpret_cast<_VScript_CompileScript>(vscriptCompileScript); + g_VScriptCompileScriptHook.SetFunc(VScript_CompileScript); + } else { + console->Warning("[sar] failed to find VScript_CompileScript at offset 0x%X\n", Offsets::VScript_CompileScript); + } } NetMessage::RegisterHandler(RESET_COOP_PROGRESS_MESSAGE_TYPE, &netResetCoopProgress); diff --git a/src/Offsets/Portal 2 9568.hpp b/src/Offsets/Portal 2 9568.hpp index 820e0826d..de29e074c 100644 --- a/src/Offsets/Portal 2 9568.hpp +++ b/src/Offsets/Portal 2 9568.hpp @@ -295,6 +295,7 @@ OFFSET_DEFAULT(GetModel, 8, 8) // Others OFFSET_DEFAULT(tickcount, 95, 64) OFFSET_DEFAULT(interval_per_tick, 65, 58) +OFFSET_DEFAULT(VScript_CompileScript, 0x28B50, 0x615D0) OFFSET_DEFAULT(GetClientStateFunction, 4, 9) OFFSET_EMPTY(cl) OFFSET_DEFAULT(demoplayer, 74, 80) From cc0aec3e04ddcf47706a0300ccfe326a7401b6b6 Mon Sep 17 00:00:00 2001 From: Betsruner <55611319+betsruner@users.noreply.github.com> Date: Thu, 30 Apr 2026 05:35:05 -0500 Subject: [PATCH 2/3] Fixed vscript checksums not queueing correctly --- src/Modules/EngineDemoRecorder.cpp | 12 ++++++++++++ src/Modules/Server.cpp | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Modules/EngineDemoRecorder.cpp b/src/Modules/EngineDemoRecorder.cpp index 987c63bbd..8f3714feb 100644 --- a/src/Modules/EngineDemoRecorder.cpp +++ b/src/Modules/EngineDemoRecorder.cpp @@ -104,6 +104,10 @@ static void RecordQueuedCommands() { } static void RecordQueuedVScriptChecksums() { + if (!engine->demorecorder->isRecordingDemo) return; + if (!engine->demorecorder->customDataReady) return; + if (engine->demorecorder->GetTick() < 0) return; + for (auto &queuedChecksum : engine->demorecorder->queuedVScriptChecksums) { size_t nameLen = queuedChecksum.first.size(); size_t bufLen = nameLen + 6; @@ -117,7 +121,15 @@ static void RecordQueuedVScriptChecksums() { engine->demorecorder->queuedVScriptChecksums.clear(); } +ON_EVENT(PRE_TICK) { + if (!engine->demorecorder->queuedVScriptChecksums.empty()) { + RecordQueuedVScriptChecksums(); + } +} + ON_EVENT(SESSION_END) { + engine->demorecorder->queuedVScriptChecksums.clear(); + if (*engine->demorecorder->m_bRecording && sar_autorecord.GetInt() == -1) { engine->demorecorder->Stop(); } diff --git a/src/Modules/Server.cpp b/src/Modules/Server.cpp index a96f1b6a2..a0dc8f95f 100644 --- a/src/Modules/Server.cpp +++ b/src/Modules/Server.cpp @@ -359,7 +359,7 @@ static void RecordRuntimeVscriptChecksum(const char *scriptName, const char *scr sum = lodepng_crc32(reinterpret_cast(scriptData), scriptLen); } - if (engine->demorecorder->isRecordingDemo) { + if (engine->demorecorder->isRecordingDemo && engine->demorecorder->GetTick() >= 0) { size_t nameLen = strlen(scriptName); size_t bufLen = nameLen + 6; auto *buf = new uint8_t[bufLen]; From 6cc54db7e5e0e4357a67223053a23053c31c2082 Mon Sep 17 00:00:00 2001 From: Betsruner <55611319+betsruner@users.noreply.github.com> Date: Fri, 15 May 2026 03:02:01 -0500 Subject: [PATCH 3/3] Fixed various issues --- src/Checksum.cpp | 36 ++++++++++++++++ src/Checksum.hpp | 1 + src/Modules.hpp | 1 + src/Modules/Server.cpp | 79 ----------------------------------- src/Modules/VScript.cpp | 38 +++++++++++++++++ src/Modules/VScript.hpp | 16 +++++++ src/Offsets/Portal 2 9568.hpp | 4 +- src/SAR.cpp | 1 + 8 files changed, 96 insertions(+), 80 deletions(-) create mode 100644 src/Modules/VScript.cpp create mode 100644 src/Modules/VScript.hpp diff --git a/src/Checksum.cpp b/src/Checksum.cpp index 178e11a79..821237a11 100644 --- a/src/Checksum.cpp +++ b/src/Checksum.cpp @@ -27,6 +27,8 @@ #define READ_LE32(arr, i) \ (((uint32_t)arr[i + 0] << 0) | ((uint32_t)arr[i + 1] << 8) | ((uint32_t)arr[i + 2] << 16) | ((uint32_t)arr[i + 3] << 24)) +static constexpr uint8_t SAR_MSG_VSCRIPT_RUNTIME_CHECKSUM = 0x14; + // clang-format off static const uint32_t crcTable[256] = { 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, @@ -507,6 +509,40 @@ static void addVpkInternalChecksum(const VpkInternalData &vpk) { engine->demorecorder->RecordData(data.data(), data.size()); } +static size_t BoundedCStringLen(const char *str, size_t maxLen) { + if (!str) return 0; + size_t len = 0; + while (len < maxLen && str[len]) ++len; + return len; +} + +void RecordRuntimeVscriptChecksum(const char *scriptName, const char *scriptData) { + if (!scriptName || !*scriptName || !scriptData || !*scriptData) return; + + // Avoid an unbounded C-string search in case file data is not null-terminated. + constexpr size_t MAX_SCRIPT_SIZE = 8 * 1024 * 1024; + size_t scriptLen = BoundedCStringLen(scriptData, MAX_SCRIPT_SIZE); + if (scriptLen == 0) return; + + uint32_t sum = 0; + if (scriptLen < MAX_SCRIPT_SIZE) { + sum = crc32(scriptData, scriptLen); + } + + if (engine->demorecorder->isRecordingDemo && engine->demorecorder->GetTick() >= 0) { + size_t nameLen = strlen(scriptName); + size_t bufLen = nameLen + 6; + auto *buf = new uint8_t[bufLen]; + buf[0] = SAR_MSG_VSCRIPT_RUNTIME_CHECKSUM; + *reinterpret_cast(buf + 1) = sum; + strcpy(reinterpret_cast(buf + 5), scriptName); + engine->demorecorder->RecordData(buf, bufLen); + delete[] buf; + } else { + engine->demorecorder->queuedVScriptChecksums.emplace_back(scriptName, sum); + } +} + void AddDemoVpkChecksums() { if (g_vpkThread.joinable()) g_vpkThread.join(); diff --git a/src/Checksum.hpp b/src/Checksum.hpp index c5d9113d4..40dc27b41 100644 --- a/src/Checksum.hpp +++ b/src/Checksum.hpp @@ -6,3 +6,4 @@ bool AddDemoChecksum(const char *filename); void AddDemoFileChecksums(); void AddDemoVpkChecksums(); +void RecordRuntimeVscriptChecksum(const char *scriptName, const char *scriptData); diff --git a/src/Modules.hpp b/src/Modules.hpp index 6e7696a4a..28ff626c8 100644 --- a/src/Modules.hpp +++ b/src/Modules.hpp @@ -12,3 +12,4 @@ #include "Modules/Tier1.hpp" #include "Modules/VGui.hpp" #include "Modules/VPhysics.hpp" +#include "Modules/VScript.hpp" diff --git a/src/Modules/Server.cpp b/src/Modules/Server.cpp index a0dc8f95f..b687276a1 100644 --- a/src/Modules/Server.cpp +++ b/src/Modules/Server.cpp @@ -41,7 +41,6 @@ #include "Offsets.hpp" #include "Utils.hpp" #include "Variable.hpp" -#include "Utils/lodepng.hpp" #include "Features/OverlayRender.hpp" @@ -337,70 +336,6 @@ static bool FindClosestPassableSpace_Detour(void *entity, const Vector &ind_push Hook FindClosestPassableSpace_Hook(&FindClosestPassableSpace_Detour); static int (*UTIL_GetCommandClientIndex)(); -static constexpr uint8_t SAR_MSG_VSCRIPT_RUNTIME_CHECKSUM = 0x14; - -static size_t BoundedCStringLen(const char *str, size_t maxLen) { - if (!str) return 0; - size_t len = 0; - while (len < maxLen && str[len]) ++len; - return len; -} - -static void RecordRuntimeVscriptChecksum(const char *scriptName, const char *scriptData) { - if (!scriptName || !*scriptName || !scriptData || !*scriptData) return; - - // Meant to avoid an unbounded C-String search in case a file data ever isn't null-terminated - constexpr size_t MAX_SCRIPT_SIZE = 8 * 1024 * 1024; - size_t scriptLen = BoundedCStringLen(scriptData, MAX_SCRIPT_SIZE); - if (scriptLen == 0) return; - - uint32_t sum = 0; - if (scriptLen < MAX_SCRIPT_SIZE) { - sum = lodepng_crc32(reinterpret_cast(scriptData), scriptLen); - } - - if (engine->demorecorder->isRecordingDemo && engine->demorecorder->GetTick() >= 0) { - size_t nameLen = strlen(scriptName); - size_t bufLen = nameLen + 6; - auto *buf = new uint8_t[bufLen]; - buf[0] = SAR_MSG_VSCRIPT_RUNTIME_CHECKSUM; - *reinterpret_cast(buf + 1) = sum; - strcpy(reinterpret_cast(buf + 5), scriptName); - engine->demorecorder->RecordData(buf, bufLen); - delete[] buf; - } else { - engine->demorecorder->queuedVScriptChecksums.emplace_back(scriptName, sum); - } -} - -#ifdef _WIN32 -using _VScript_CompileScript = int(__rescall *)(void *thisptr, const char *scriptData, const char *scriptName); -#else -using _VScript_CompileScript = int(__cdecl *)(void *thisptr, const char *scriptData, const char *scriptName); -#endif -static _VScript_CompileScript VScript_CompileScript; -extern Hook g_VScriptCompileScriptHook; -#ifdef _WIN32 -static int __fastcall VScript_CompileScript_Hook(void *thisptr, int edx, const char *scriptData, const char *scriptName) -#else -static int __cdecl VScript_CompileScript_Hook(void *thisptr, const char *scriptData, const char *scriptName) -#endif -{ -#ifdef _WIN32 - (void)edx; -#endif - (void)thisptr; - - if (scriptName && *scriptName) { - RecordRuntimeVscriptChecksum(scriptName, scriptData); - } - - g_VScriptCompileScriptHook.Disable(); - auto ret = VScript_CompileScript(thisptr, scriptData, scriptName); - g_VScriptCompileScriptHook.Enable(); - return ret; -} -Hook g_VScriptCompileScriptHook(&VScript_CompileScript_Hook); extern Hook g_ViewPunch_Hook; DETOUR_T(void, Server::ViewPunch, const QAngle &offset) { @@ -1034,20 +969,6 @@ bool Server::Init() { if (sar.game->Is(SourceGame_Portal2 | SourceGame_Portal2_2011)) { Server::IsInPVS = (Server::_IsInPVS)Memory::Scan(this->Name(), Offsets::IsInPVS); g_IsInPVS_Hook.SetFunc(IsInPVS); - -#ifdef _WIN32 - const char *vscriptModuleName = "vscript.dll"; -#else - const char *vscriptModuleName = "vscript.so"; -#endif - - auto vscriptCompileScript = Memory::Absolute(vscriptModuleName, Offsets::VScript_CompileScript); - if (vscriptCompileScript) { - VScript_CompileScript = reinterpret_cast<_VScript_CompileScript>(vscriptCompileScript); - g_VScriptCompileScriptHook.SetFunc(VScript_CompileScript); - } else { - console->Warning("[sar] failed to find VScript_CompileScript at offset 0x%X\n", Offsets::VScript_CompileScript); - } } NetMessage::RegisterHandler(RESET_COOP_PROGRESS_MESSAGE_TYPE, &netResetCoopProgress); diff --git a/src/Modules/VScript.cpp b/src/Modules/VScript.cpp new file mode 100644 index 000000000..b40d70376 --- /dev/null +++ b/src/Modules/VScript.cpp @@ -0,0 +1,38 @@ +#include "VScript.hpp" + +#include "Checksum.hpp" +#include "Game.hpp" +#include "Hook.hpp" +#include "Offsets.hpp" +#include "SAR.hpp" + +REDECL(VScript::CompileScript); + +extern Hook g_VScriptCompileScriptHook; +DETOUR_T(int, VScript::CompileScript, const char *scriptData, const char *scriptName) { + if (scriptName && *scriptName) { + RecordRuntimeVscriptChecksum(scriptName, scriptData); + } + + g_VScriptCompileScriptHook.Disable(); + auto ret = VScript::CompileScript(thisptr, scriptData, scriptName); + g_VScriptCompileScriptHook.Enable(); + return ret; +} +Hook g_VScriptCompileScriptHook(&VScript::CompileScript_Hook); + +bool VScript::Init() { + VScript::CompileScript = (VScript::_CompileScript)Memory::Scan(this->Name(), Offsets::VScript_CompileScript); + if (!VScript::CompileScript) { + console->Warning("[sar] failed to find VScript_CompileScript\n"); + return false; + } + + g_VScriptCompileScriptHook.SetFunc(VScript::CompileScript); + return this->hasLoaded = true; +} + +void VScript::Shutdown() { +} + +VScript *vscript; diff --git a/src/Modules/VScript.hpp b/src/Modules/VScript.hpp new file mode 100644 index 000000000..bddd5d65c --- /dev/null +++ b/src/Modules/VScript.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "Module.hpp" +#include "Utils.hpp" + +class VScript : public Module { +public: + // CVScriptGameSystem::CompileScript + DECL_DETOUR_T(int, CompileScript, const char *scriptData, const char *scriptName); + + bool Init() override; + void Shutdown() override; + const char *Name() override { return MODULE("vscript"); } +}; + +extern VScript *vscript; diff --git a/src/Offsets/Portal 2 9568.hpp b/src/Offsets/Portal 2 9568.hpp index de29e074c..d9abb13c4 100644 --- a/src/Offsets/Portal 2 9568.hpp +++ b/src/Offsets/Portal 2 9568.hpp @@ -295,7 +295,6 @@ OFFSET_DEFAULT(GetModel, 8, 8) // Others OFFSET_DEFAULT(tickcount, 95, 64) OFFSET_DEFAULT(interval_per_tick, 65, 58) -OFFSET_DEFAULT(VScript_CompileScript, 0x28B50, 0x615D0) OFFSET_DEFAULT(GetClientStateFunction, 4, 9) OFFSET_EMPTY(cl) OFFSET_DEFAULT(demoplayer, 74, 80) @@ -546,6 +545,9 @@ OFFSET_DEFAULT(Portal2PromoFlagsOff, 2, 1) OFFSET_EMPTY(DestroyEnvironment) OFFSET_EMPTY(GetActiveEnvironmentByIndex) +// Vscript +SIGSCAN_DEFAULT(VScript_CompileScript, "55 8B EC 83 EC 0C 56 57 8B 7D 08 8B F1 85 FF 0F 84 ? ? ? ? 80 3F 00 0F 84 ? ? ? ? 53 8B", "57 56 53 8B 5C 24 14 8B 7C 24 10 8B 74 24 18 85 DB 74 ? 80 3B 00 74 ? 85 F6 B8 ? ? ? ? 0F 44 F0 83 EC 0C 53 E8 ? ? ? ? C7 04 24 01 00") + // Steam API SIGSCAN_DEFAULT(interfaceMgrSig, "89 0D ? ? ? ? 85 C9 0F", "") diff --git a/src/SAR.cpp b/src/SAR.cpp index d2b611239..fd2d75260 100644 --- a/src/SAR.cpp +++ b/src/SAR.cpp @@ -101,6 +101,7 @@ bool SAR::Load(CreateInterfaceFn interfaceFactory, CreateInterfaceFn gameServerF this->modules->AddModule(&matchmaking); this->modules->AddModule(&steam); this->modules->AddModule(&vphysics); + this->modules->AddModule(&vscript); this->modules->InitAll(); SarInitHandler::RunAll();