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/2] 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/2] 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];