From fbb4b6fca826d45fdfbbaedc8f6344c63344cc33 Mon Sep 17 00:00:00 2001 From: Betsruner <55611319+betsruner@users.noreply.github.com> Date: Mon, 25 May 2026 21:45:29 -0500 Subject: [PATCH 1/3] First working version --- src/Features/DataCacheTools.cpp | 151 +++++++++++ src/Features/DataCacheTools.hpp | 6 + src/Features/ModelCacheTools.cpp | 435 +++++++++++++++++++++++++++++++ src/Features/ModelCacheTools.hpp | 6 + src/Modules/Engine.cpp | 13 +- src/Modules/EngineDemoPlayer.cpp | 15 +- 6 files changed, 619 insertions(+), 7 deletions(-) create mode 100644 src/Features/DataCacheTools.cpp create mode 100644 src/Features/DataCacheTools.hpp create mode 100644 src/Features/ModelCacheTools.cpp create mode 100644 src/Features/ModelCacheTools.hpp diff --git a/src/Features/DataCacheTools.cpp b/src/Features/DataCacheTools.cpp new file mode 100644 index 00000000..d1033540 --- /dev/null +++ b/src/Features/DataCacheTools.cpp @@ -0,0 +1,151 @@ +#include "DataCacheTools.hpp" + +#include "Command.hpp" +#include "Game.hpp" +#include "Interface.hpp" +#include "Modules/Console.hpp" +#include "Modules/Engine.hpp" +#include "Utils.hpp" +#include "Utils/Memory.hpp" +#include "Variable.hpp" + +#include +#include +#include +#include + +namespace { +constexpr int DATA_CACHE_SET_SIZE_VTABLE_INDEX = 8; +constexpr int BYTES_PER_MIB = 1024 * 1024; + +using DataCacheSetSizeFn = void(__rescall *)(void *thisptr, int bytes); + +Variable sar_demo_datacache_flush_between_demos( + "sar_demo_datacache_flush_between_demos", + "0", + 0, + 2, + "Experimental repeated-demo cache flush before queued demos. 0=off, 1=flush, 2=flush_locked. Waits for idle menu state before flushing. Mode 2 also requires sar_demo_datacache_allow_unsafe_flush_locked 1.\n"); +Variable sar_demo_datacache_flush_delay_frames( + "sar_demo_datacache_flush_delay_frames", + "120", + 0, + "Experimental repeated-demo cache flush delay. Number of idle menu frames to wait before flushing.\n"); +Variable sar_demo_datacache_allow_unsafe_flush_locked( + "sar_demo_datacache_allow_unsafe_flush_locked", + "0", + "Unsafe experiment. Allows queued flush_locked between demos; this has crashed during the next demo load.\n"); + +bool g_pendingBetweenDemoFlush = false; +int g_waitingForMenuFrames = 0; +int g_idleMenuFrames = 0; + +void *GetDataCache() { + static void *dataCache = nullptr; + if (!dataCache) { + dataCache = Interface::GetPtr(MODULE("datacache"), "VDataCache003"); + } + return dataCache; +} + +bool ParseMiB(const char *text, int &out) { + char *end = nullptr; + errno = 0; + auto value = std::strtol(text, &end, 10); + if (errno || end == text || *end || value <= 0 || value > INT_MAX / BYTES_PER_MIB) { + return false; + } + out = static_cast(value); + return true; +} + +bool IsIdleMenuState() { + if (!engine || !engine->hoststate || !engine->m_szLevelName || !engine->demoplayer) return false; + + auto hoststateRun = HS_RUN; + if (sar.game->Is(SourceGame_INFRA)) hoststateRun = INFRA_HS_RUN; + + return engine->hoststate->m_currentState == hoststateRun + && !engine->hoststate->m_activeGame + && !engine->demoplayer->IsPlaying() + && std::strlen(engine->m_szLevelName) == 0 + && engine->GetMaxClients() <= 1; +} + +const char *FlushCommandForMode(int mode) { + return mode >= 2 ? "flush_locked" : "flush"; +} +} // namespace + +namespace DataCacheTools { +void AfterDemoStop() { + if (sar_demo_datacache_flush_between_demos.GetInt() > 0) { + g_pendingBetweenDemoFlush = true; + g_waitingForMenuFrames = 0; + g_idleMenuFrames = 0; + } +} + +bool BeforeQueuedDemoStart() { + auto mode = sar_demo_datacache_flush_between_demos.GetInt(); + if (mode <= 0) { + g_pendingBetweenDemoFlush = false; + return true; + } + + if (!g_pendingBetweenDemoFlush) return true; + if (mode >= 2 && !sar_demo_datacache_allow_unsafe_flush_locked.GetBool()) { + console->Warning("SAR: Refusing queued flush_locked; set sar_demo_datacache_allow_unsafe_flush_locked 1 to force this unsafe experiment.\n"); + g_pendingBetweenDemoFlush = false; + g_waitingForMenuFrames = 0; + g_idleMenuFrames = 0; + return true; + } + + if (!IsIdleMenuState()) { + if (g_waitingForMenuFrames == 0 || g_waitingForMenuFrames % 60 == 0) { + console->Print("Waiting for idle menu state before demo datacache flush.\n"); + } + ++g_waitingForMenuFrames; + g_idleMenuFrames = 0; + return false; + } + + auto delayFrames = sar_demo_datacache_flush_delay_frames.GetInt(); + if (g_idleMenuFrames < delayFrames) { + if (g_idleMenuFrames == 0 || g_idleMenuFrames % 60 == 0) { + console->Print("Waiting %d idle menu frames before demo datacache flush.\n", delayFrames - g_idleMenuFrames); + } + ++g_idleMenuFrames; + return false; + } + + auto command = FlushCommandForMode(mode); + console->Print("Running %s before queued demo start.\n", command); + engine->ExecuteCommand(command, true); + g_pendingBetweenDemoFlush = false; + g_waitingForMenuFrames = 0; + g_idleMenuFrames = 0; + return true; +} +} // namespace DataCacheTools + +CON_COMMAND(sar_datacache_set_size_mb, "sar_datacache_set_size_mb - directly sets VDataCache capacity, bypassing datacachesize cvar limits\n") { + if (args.ArgC() != 2) { + return console->Print(sar_datacache_set_size_mb.ThisPtr()->m_pszHelpString); + } + + int mib = 0; + if (!ParseMiB(args[1], mib)) { + return console->Print("Invalid datacache size. Use a positive integer MiB value.\n"); + } + + auto dataCache = GetDataCache(); + if (!dataCache) { + return console->Warning("SAR: Failed to get VDataCache003 from datacache.\n"); + } + + auto setSize = Memory::VMT(dataCache, DATA_CACHE_SET_SIZE_VTABLE_INDEX); + setSize(dataCache, mib * BYTES_PER_MIB); + console->Print("Set VDataCache capacity to %d MB. Run cache_print_summary to confirm.\n", mib); +} diff --git a/src/Features/DataCacheTools.hpp b/src/Features/DataCacheTools.hpp new file mode 100644 index 00000000..086da216 --- /dev/null +++ b/src/Features/DataCacheTools.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace DataCacheTools { + void AfterDemoStop(); + bool BeforeQueuedDemoStart(); +} diff --git a/src/Features/ModelCacheTools.cpp b/src/Features/ModelCacheTools.cpp new file mode 100644 index 00000000..a3a8f925 --- /dev/null +++ b/src/Features/ModelCacheTools.cpp @@ -0,0 +1,435 @@ +#include "ModelCacheTools.hpp" + +#include "Command.hpp" +#include "Modules/Console.hpp" +#include "Utils.hpp" +#include "Utils/Memory.hpp" +#include "Variable.hpp" + +namespace { +constexpr int CMODELLOADER_FULL_RESET_VTABLE_INDEX = 1; +constexpr int CMODELLOADER_STRONG_CLEANUP_VTABLE_INDEX = 12; +constexpr int CMODELLOADER_ENTRY_ARRAY_OFFSET = 0x8; +constexpr int CMODELLOADER_ENTRY_COUNT_OFFSET = 0x16; +constexpr int CMODELLOADER_ENTRY_STRIDE = 0x10; +constexpr int CMODELLOADER_ENTRY_MODEL_OFFSET = 0xC; +constexpr int MODEL_FLAGS_OFFSET = 0x108; +constexpr unsigned int MODEL_FLAG_PROTECTED_MASK = 0x7E; +constexpr unsigned int MODEL_FLAG_PROTECTED_PRELOAD = 0x4; +constexpr unsigned int MODEL_FLAG_SKIP_PROTECTED_CLEAR = 0x40; +constexpr unsigned int MAX_REASONABLE_MODEL_COUNT = 16384; + +using CModelLoaderStrongCleanupFn = void(__rescall *)(void *thisptr); +using CModelLoaderFullResetFn = void(__rescall *)(void *thisptr); + +Variable sar_demo_modelcache_cleanup( + "sar_demo_modelcache_cleanup", + "0", + 0, + 3, + "Experimental repeated-demo cleanup. 0=off, 1=after demo stop, 2=before next demo start, 3=both.\n"); +Variable sar_demo_modelcache_unprotect_models( + "sar_demo_modelcache_unprotect_models", + "0", + "Experimental repeated-demo patch. During demo playback, load client model precache/consistency models with flag 0 instead of protected flag 4.\n"); +Variable sar_demo_modelcache_clear_flag4( + "sar_demo_modelcache_clear_flag4", + "0", + 0, + 3, + "Experimental repeated-demo cleanup. 0=off, 1=after demo stop, 2=before next demo start, 3=both. Clears CModelLoader model flag 4; automatic strong cleanup is skipped unless sar_demo_modelcache_cleanup_after_clear is set.\n"); +Variable sar_demo_modelcache_clear_protected_flags( + "sar_demo_modelcache_clear_protected_flags", + "0", + 0, + 3, + "Experimental repeated-demo cleanup. 0=off, 1=after demo stop, 2=before next demo start, 3=both. Clears CModelLoader protected flag mask 0x7E without unloading models; models with bit 0x40 are skipped.\n"); +Variable sar_demo_modelcache_cleanup_after_clear( + "sar_demo_modelcache_cleanup_after_clear", + "0", + "Unsafe experiment. If set, automatic strong cleanup is allowed to run while CModelLoader protected-flag clearing is enabled.\n"); +Variable sar_demo_modelcache_full_reset( + "sar_demo_modelcache_full_reset", + "0", + 0, + 3, + "Experimental repeated-demo CModelLoader full reset. 0=off, 1=after demo stop, 2=before next demo start, 3=both.\n"); +Variable sar_demo_modelcache_report( + "sar_demo_modelcache_report", + "0", + 0, + 3, + "Repeated-demo diagnostic. 0=off, 1=after demo stop, 2=before next demo start, 3=both. Reports CModelLoader model flag counts without changing state.\n"); + +bool g_cleanupPendingBeforeStart = false; +Memory::Patch *g_modelPrecacheFlagPatch = nullptr; +Memory::Patch *g_modelConsistencyFlagPatch = nullptr; + +bool IsAddressInModule(uintptr_t address, const char *moduleName) { + Memory::ModuleInfo info = {}; + if (!Memory::TryGetModule(moduleName, &info)) return false; + + return address >= info.base && address < info.base + info.size; +} + +uintptr_t ReadEngineGlobalFromPattern(uintptr_t site, int immediateOffset, const char *name) { + if (!site) return 0; + + auto candidate = *reinterpret_cast(site + immediateOffset); + if (!IsAddressInModule(candidate, MODULE("engine"))) { + console->Warning("SAR: Ignoring suspicious %s global address %p.\n", name, reinterpret_cast(candidate)); + return 0; + } + + return candidate; +} + +uintptr_t FindModelLoaderGlobal() { +#ifdef _WIN32 + static uintptr_t global = 0; + if (global) return global; + + // SV_ActivateServer-specific context for: mov ecx, g_pModelLoader; vtable +0x30. + uintptr_t site = Memory::Scan(MODULE("engine"), "80 3D ? ? ? ? 00 74 0D 8B 0D ? ? ? ? 8B 01 8B 50 30 FF D2 83 3D ? ? ? ? 01 7F"); + global = ReadEngineGlobalFromPattern(site, 11, "SV_ActivateServer CModelLoader"); + if (global) return global; + + // Client modelprecache-specific context for: g_pModelLoader->FindOrLoadModel(name, 4). + site = Memory::Scan(MODULE("engine"), "8B 0D ? ? ? ? 8B 11 6A 04 50 8B 42 1C FF D0 50 EB 02 6A 00"); + global = ReadEngineGlobalFromPattern(site, 2, "modelprecache CModelLoader"); + if (global) return global; + + // Client consistency-check model path uses the same global. + site = Memory::Scan(MODULE("engine"), "8B 0D ? ? ? ? 8B 01 8B 50 1C 6A 04 53 FF D2 8B F8"); + global = ReadEngineGlobalFromPattern(site, 2, "consistency CModelLoader"); + if (global) return global; + + return 0; +#else + return 0; +#endif +} + +void *GetModelLoader() { + auto global = FindModelLoaderGlobal(); + if (!global) return nullptr; + + return *reinterpret_cast(global); +} + +bool GetModelTable(uintptr_t &modelLoader, uintptr_t &entries, unsigned short &count, const char *action) { + modelLoader = reinterpret_cast(GetModelLoader()); + if (!modelLoader) { + console->Warning("SAR: Failed to find CModelLoader for %s.\n", action); + return false; + } + + entries = *reinterpret_cast(modelLoader + CMODELLOADER_ENTRY_ARRAY_OFFSET); + count = *reinterpret_cast(modelLoader + CMODELLOADER_ENTRY_COUNT_OFFSET); + if (!entries || count > MAX_REASONABLE_MODEL_COUNT) { + console->Warning( + "SAR: Refusing CModelLoader %s with suspicious state (loader=%p entries=%p count=%u).\n", + action, + reinterpret_cast(modelLoader), + reinterpret_cast(entries), + count); + return false; + } + + return true; +} + +bool RunStrongCleanup(const char *reason, bool verbose) { + auto modelLoader = GetModelLoader(); + if (!modelLoader) { + if (verbose) console->Warning("SAR: Failed to find CModelLoader for model-cache cleanup.\n"); + return false; + } + + auto cleanup = Memory::VMT(modelLoader, CMODELLOADER_STRONG_CLEANUP_VTABLE_INDEX); + if (!cleanup) { + if (verbose) console->Warning("SAR: Failed to find CModelLoader strong cleanup method.\n"); + return false; + } + + cleanup(modelLoader); + if (verbose) console->Print("Ran CModelLoader strong cleanup (%s).\n", reason); + return true; +} + +bool RunFullReset(const char *reason, bool verbose) { + auto modelLoader = GetModelLoader(); + if (!modelLoader) { + if (verbose) console->Warning("SAR: Failed to find CModelLoader for model-cache full reset.\n"); + return false; + } + + auto fullReset = Memory::VMT(modelLoader, CMODELLOADER_FULL_RESET_VTABLE_INDEX); + if (!fullReset) { + if (verbose) console->Warning("SAR: Failed to find CModelLoader full reset method.\n"); + return false; + } + + fullReset(modelLoader); + if (verbose) console->Print("Ran CModelLoader full reset (%s).\n", reason); + return true; +} + +bool ShouldCleanup(int bit) { + return (sar_demo_modelcache_cleanup.GetInt() & bit) != 0; +} + +bool ShouldClearFlag4(int bit) { + return (sar_demo_modelcache_clear_flag4.GetInt() & bit) != 0; +} + +bool ShouldClearProtectedFlags(int bit) { + return (sar_demo_modelcache_clear_protected_flags.GetInt() & bit) != 0; +} + +bool ShouldFullReset(int bit) { + return (sar_demo_modelcache_full_reset.GetInt() & bit) != 0; +} + +bool ShouldReport(int bit) { + return (sar_demo_modelcache_report.GetInt() & bit) != 0; +} + +bool RunAutomaticStrongCleanup(const char *reason) { + if ((sar_demo_modelcache_clear_flag4.GetInt() || sar_demo_modelcache_clear_protected_flags.GetInt()) + && !sar_demo_modelcache_cleanup_after_clear.GetBool()) { + console->Print("Skipping CModelLoader strong cleanup (%s) because protected-flag clearing is active.\n", reason); + return false; + } + + return RunStrongCleanup(reason, true); +} + +int ClearModelFlag4(const char *reason, bool verbose) { + uintptr_t modelLoader = 0; + uintptr_t entries = 0; + unsigned short count = 0; + if (!GetModelTable(modelLoader, entries, count, "model flag cleanup")) return 0; + + if (verbose) { + console->Print( + "Scanning CModelLoader flag 4 (%s, loader=%p entries=%p count=%u).\n", + reason, + reinterpret_cast(modelLoader), + reinterpret_cast(entries), + count); + } + + int cleared = 0; + for (unsigned int i = 0; i < count; ++i) { + auto model = *reinterpret_cast(entries + i * CMODELLOADER_ENTRY_STRIDE + CMODELLOADER_ENTRY_MODEL_OFFSET); + if (!model) continue; + + auto flags = reinterpret_cast(model + MODEL_FLAGS_OFFSET); + if ((*flags & MODEL_FLAG_PROTECTED_PRELOAD) == 0) continue; + + *flags &= ~MODEL_FLAG_PROTECTED_PRELOAD; + ++cleared; + } + + if (verbose) console->Print("Cleared CModelLoader flag 4 from %d models (%s).\n", cleared, reason); + return cleared; +} + +int ClearModelProtectedFlags(const char *reason, bool verbose) { + uintptr_t modelLoader = 0; + uintptr_t entries = 0; + unsigned short count = 0; + if (!GetModelTable(modelLoader, entries, count, "model protected-flag cleanup")) return 0; + + if (verbose) { + console->Print( + "Scanning CModelLoader protected flags (%s, loader=%p entries=%p count=%u).\n", + reason, + reinterpret_cast(modelLoader), + reinterpret_cast(entries), + count); + } + + int cleared = 0; + int skippedBit40 = 0; + for (unsigned int i = 0; i < count; ++i) { + auto model = *reinterpret_cast(entries + i * CMODELLOADER_ENTRY_STRIDE + CMODELLOADER_ENTRY_MODEL_OFFSET); + if (!model) continue; + + auto flags = reinterpret_cast(model + MODEL_FLAGS_OFFSET); + if ((*flags & MODEL_FLAG_SKIP_PROTECTED_CLEAR) != 0) { + ++skippedBit40; + continue; + } + + if ((*flags & MODEL_FLAG_PROTECTED_MASK) == 0) continue; + *flags &= ~MODEL_FLAG_PROTECTED_MASK; + ++cleared; + } + + if (verbose) { + console->Print( + "Cleared CModelLoader protected flags 0x7E from %d models; skipped %d bit0x40 models (%s).\n", + cleared, + skippedBit40, + reason); + } + return cleared; +} + +void ReportModelFlags(const char *reason) { + uintptr_t modelLoader = 0; + uintptr_t entries = 0; + unsigned short count = 0; + if (!GetModelTable(modelLoader, entries, count, "model flag report")) return; + + unsigned int models = 0; + unsigned int zero = 0; + unsigned int protectedFlags = 0; + unsigned int bit2 = 0; + unsigned int bit4 = 0; + unsigned int bit8 = 0; + unsigned int bit10 = 0; + unsigned int bit20 = 0; + unsigned int bit40 = 0; + + for (unsigned int i = 0; i < count; ++i) { + auto model = *reinterpret_cast(entries + i * CMODELLOADER_ENTRY_STRIDE + CMODELLOADER_ENTRY_MODEL_OFFSET); + if (!model) continue; + + ++models; + auto flags = *reinterpret_cast(model + MODEL_FLAGS_OFFSET); + if (flags == 0) ++zero; + if (flags & MODEL_FLAG_PROTECTED_MASK) ++protectedFlags; + if (flags & 0x2) ++bit2; + if (flags & 0x4) ++bit4; + if (flags & 0x8) ++bit8; + if (flags & 0x10) ++bit10; + if (flags & 0x20) ++bit20; + if (flags & 0x40) ++bit40; + } + + console->Print( + "CModelLoader report (%s): loader=%p entries=%p count=%u models=%u.\n", + reason, + reinterpret_cast(modelLoader), + reinterpret_cast(entries), + count, + models); + console->Print( + "CModelLoader flags (%s): zero=%u protected0x7e=%u bit0x02=%u bit0x04=%u bit0x08=%u bit0x10=%u bit0x20=%u bit0x40=%u.\n", + reason, + zero, + protectedFlags, + bit2, + bit4, + bit8, + bit10, + bit20, + bit40); +} + +bool InitFlagPatch(Memory::Patch *&patch, const char *pattern, int immediateOffset, const char *name) { + if (!patch) patch = new Memory::Patch(); + if (patch->IsInit()) return true; + +#ifdef _WIN32 + auto site = Memory::Scan(MODULE("engine"), pattern); + if (!site) { + console->Warning("SAR: Failed to find %s model flag patch site.\n", name); + return false; + } + + unsigned char zeroFlag = 0; + if (!patch->Execute(site + immediateOffset, &zeroFlag, 1)) { + console->Warning("SAR: Failed to apply %s model flag patch.\n", name); + return false; + } + patch->Restore(); + return true; +#else + return false; +#endif +} + +void SetFlagPatches(bool enabled) { + if (!sar_demo_modelcache_unprotect_models.GetBool()) enabled = false; + if (!enabled) { + if (g_modelPrecacheFlagPatch) g_modelPrecacheFlagPatch->Restore(); + if (g_modelConsistencyFlagPatch) g_modelConsistencyFlagPatch->Restore(); + return; + } + + bool ready = InitFlagPatch( + g_modelPrecacheFlagPatch, + "8B 0D ? ? ? ? 8B 11 6A 04 50 8B 42 1C FF D0 50 EB 02 6A 00", + 9, + "modelprecache"); + ready = InitFlagPatch( + g_modelConsistencyFlagPatch, + "8B 0D ? ? ? ? 8B 01 8B 50 1C 6A 04 53 FF D2 8B F8", + 12, + "consistency") && ready; + if (!ready) return; + + g_modelPrecacheFlagPatch->Execute(); + g_modelConsistencyFlagPatch->Execute(); + console->Print("Patched demo model loads to use unprotected flag 0.\n"); +} +} // namespace + +namespace ModelCacheTools { +void CleanupAfterDemoStop() { + g_cleanupPendingBeforeStart = true; + bool report = ShouldReport(1); + bool mutating = ShouldFullReset(1) || ShouldClearProtectedFlags(1) || ShouldClearFlag4(1) || ShouldCleanup(1); + if (report && mutating) ReportModelFlags("after demo stop before cleanup"); + if (ShouldFullReset(1)) RunFullReset("after demo stop", true); + if (ShouldClearProtectedFlags(1)) ClearModelProtectedFlags("after demo stop", true); + if (ShouldClearFlag4(1)) ClearModelFlag4("after demo stop", true); + if (ShouldCleanup(1)) { + RunAutomaticStrongCleanup("after demo stop"); + } + if (report) ReportModelFlags(mutating ? "after demo stop after cleanup" : "after demo stop"); + SetFlagPatches(false); +} + +void CleanupBeforeDemoStart() { + if (g_cleanupPendingBeforeStart) { + g_cleanupPendingBeforeStart = false; + bool report = ShouldReport(2); + bool mutating = ShouldFullReset(2) || ShouldClearProtectedFlags(2) || ShouldClearFlag4(2) || ShouldCleanup(2); + if (report && mutating) ReportModelFlags("before demo start before cleanup"); + if (ShouldFullReset(2)) RunFullReset("before demo start", true); + if (ShouldClearProtectedFlags(2)) ClearModelProtectedFlags("before demo start", true); + if (ShouldClearFlag4(2)) ClearModelFlag4("before demo start", true); + + if (ShouldCleanup(2)) { + RunAutomaticStrongCleanup("before demo start"); + } + if (report) ReportModelFlags(mutating ? "before demo start after cleanup" : "before demo start"); + } + SetFlagPatches(true); +} +} // namespace ModelCacheTools + +CON_COMMAND(sar_modelcache_strong_cleanup, "sar_modelcache_strong_cleanup - manually runs CModelLoader's stronger stale-flag cleanup\n") { + RunStrongCleanup("manual command", true); +} + +CON_COMMAND(sar_modelcache_full_reset, "sar_modelcache_full_reset - manually runs CModelLoader's full reset/unload path\n") { + RunFullReset("manual command", true); +} + +CON_COMMAND(sar_modelcache_clear_flag4, "sar_modelcache_clear_flag4 - manually clears CModelLoader model flag 4 without running stronger cleanup\n") { + ClearModelFlag4("manual command", true); +} + +CON_COMMAND(sar_modelcache_clear_protected_flags, "sar_modelcache_clear_protected_flags - manually clears CModelLoader protected flags 0x7E without unloading models\n") { + ClearModelProtectedFlags("manual command", true); +} + +CON_COMMAND(sar_modelcache_report, "sar_modelcache_report - reports CModelLoader model flag counts without changing state\n") { + ReportModelFlags("manual command"); +} diff --git a/src/Features/ModelCacheTools.hpp b/src/Features/ModelCacheTools.hpp new file mode 100644 index 00000000..eee844d3 --- /dev/null +++ b/src/Features/ModelCacheTools.hpp @@ -0,0 +1,6 @@ +#pragma once + +namespace ModelCacheTools { + void CleanupAfterDemoStop(); + void CleanupBeforeDemoStart(); +} diff --git a/src/Modules/Engine.cpp b/src/Modules/Engine.cpp index 2a38e91d..b809aeaf 100644 --- a/src/Modules/Engine.cpp +++ b/src/Modules/Engine.cpp @@ -10,6 +10,7 @@ #include "Features/AchievementTracker.hpp" #include "Features/Camera.hpp" #include "Features/Cvars.hpp" +#include "Features/DataCacheTools.hpp" #include "Features/Demo/DemoParser.hpp" #include "Features/Demo/NetworkGhostPlayer.hpp" #include "Features/Hud/PerformanceHud.hpp" @@ -403,11 +404,13 @@ DETOUR(Engine::Frame) { //demoplayer if (engine->demoplayer->demoQueueSize > 0 && !engine->demoplayer->IsPlaying() && engine->demoplayer->IsPlaybackFixReady()) { - DemoParser parser; - auto name = engine->demoplayer->demoQueue[engine->demoplayer->currentDemoID]; - engine->ExecuteCommand(Utils::ssprintf("playdemo \"%s\"", name.c_str()).c_str(), true); - if (++engine->demoplayer->currentDemoID >= engine->demoplayer->demoQueueSize) { - engine->demoplayer->ClearDemoQueue(); + if (DataCacheTools::BeforeQueuedDemoStart()) { + DemoParser parser; + auto name = engine->demoplayer->demoQueue[engine->demoplayer->currentDemoID]; + engine->ExecuteCommand(Utils::ssprintf("playdemo \"%s\"", name.c_str()).c_str(), true); + if (++engine->demoplayer->currentDemoID >= engine->demoplayer->demoQueueSize) { + engine->demoplayer->ClearDemoQueue(); + } } } diff --git a/src/Modules/EngineDemoPlayer.cpp b/src/Modules/EngineDemoPlayer.cpp index 86987ede..17442313 100644 --- a/src/Modules/EngineDemoPlayer.cpp +++ b/src/Modules/EngineDemoPlayer.cpp @@ -6,8 +6,10 @@ #include "Engine.hpp" #include "Event.hpp" #include "Features/Camera.hpp" +#include "Features/DataCacheTools.hpp" #include "Features/Demo/Demo.hpp" #include "Features/Demo/DemoParser.hpp" +#include "Features/ModelCacheTools.hpp" #include "Features/Renderer.hpp" #include "Hook.hpp" #include "Interface.hpp" @@ -235,6 +237,8 @@ DETOUR_COMMAND(EngineDemoPlayer::stopdemo) { // CDemoPlayer::StartPlayback DETOUR(EngineDemoPlayer::StartPlayback, const char *filename, bool bAsTimeDemo) { + ModelCacheTools::CleanupBeforeDemoStart(); + auto path = std::string(filename); path = fileSystem->FindFileSomewhere(path).value_or(path); auto newFilename = path.c_str(); @@ -276,10 +280,17 @@ DETOUR(EngineDemoPlayer::StartPlayback, const char *filename, bool bAsTimeDemo) // CDemoPlayer::StopPlayback DETOUR(EngineDemoPlayer::StopPlayback) { - if (engine->demoplayer->IsPlaying() && !g_demoFixing) { + bool stoppedDemo = engine->demoplayer->IsPlaying() && !g_demoFixing; + if (stoppedDemo) { Event::Trigger({}); } - return EngineDemoPlayer::StopPlayback(thisptr); + + auto result = EngineDemoPlayer::StopPlayback(thisptr); + if (stoppedDemo) { + ModelCacheTools::CleanupAfterDemoStop(); + DataCacheTools::AfterDemoStop(); + } + return result; } Variable sar_demo_portal_interp_fix("sar_demo_portal_interp_fix", "1", "Fix eye interpolation through portals in demo playback.\n"); From 43dbf7a4946ba453e71c5996b78a6b2a4312b8ef Mon Sep 17 00:00:00 2001 From: Betsruner <55611319+betsruner@users.noreply.github.com> Date: Wed, 27 May 2026 07:40:36 -0500 Subject: [PATCH 2/3] Cleanup --- src/Features/DataCacheTools.cpp | 151 --------- src/Features/DataCacheTools.hpp | 6 - src/Features/Demo/ModelCacheTools.cpp | 73 +++++ src/Features/ModelCacheTools.cpp | 435 -------------------------- src/Features/ModelCacheTools.hpp | 6 - src/Modules/Engine.cpp | 13 +- src/Modules/EngineDemoPlayer.cpp | 15 +- src/Offsets/Portal 2 9568.hpp | 16 +- 8 files changed, 95 insertions(+), 620 deletions(-) delete mode 100644 src/Features/DataCacheTools.cpp delete mode 100644 src/Features/DataCacheTools.hpp create mode 100644 src/Features/Demo/ModelCacheTools.cpp delete mode 100644 src/Features/ModelCacheTools.cpp delete mode 100644 src/Features/ModelCacheTools.hpp diff --git a/src/Features/DataCacheTools.cpp b/src/Features/DataCacheTools.cpp deleted file mode 100644 index d1033540..00000000 --- a/src/Features/DataCacheTools.cpp +++ /dev/null @@ -1,151 +0,0 @@ -#include "DataCacheTools.hpp" - -#include "Command.hpp" -#include "Game.hpp" -#include "Interface.hpp" -#include "Modules/Console.hpp" -#include "Modules/Engine.hpp" -#include "Utils.hpp" -#include "Utils/Memory.hpp" -#include "Variable.hpp" - -#include -#include -#include -#include - -namespace { -constexpr int DATA_CACHE_SET_SIZE_VTABLE_INDEX = 8; -constexpr int BYTES_PER_MIB = 1024 * 1024; - -using DataCacheSetSizeFn = void(__rescall *)(void *thisptr, int bytes); - -Variable sar_demo_datacache_flush_between_demos( - "sar_demo_datacache_flush_between_demos", - "0", - 0, - 2, - "Experimental repeated-demo cache flush before queued demos. 0=off, 1=flush, 2=flush_locked. Waits for idle menu state before flushing. Mode 2 also requires sar_demo_datacache_allow_unsafe_flush_locked 1.\n"); -Variable sar_demo_datacache_flush_delay_frames( - "sar_demo_datacache_flush_delay_frames", - "120", - 0, - "Experimental repeated-demo cache flush delay. Number of idle menu frames to wait before flushing.\n"); -Variable sar_demo_datacache_allow_unsafe_flush_locked( - "sar_demo_datacache_allow_unsafe_flush_locked", - "0", - "Unsafe experiment. Allows queued flush_locked between demos; this has crashed during the next demo load.\n"); - -bool g_pendingBetweenDemoFlush = false; -int g_waitingForMenuFrames = 0; -int g_idleMenuFrames = 0; - -void *GetDataCache() { - static void *dataCache = nullptr; - if (!dataCache) { - dataCache = Interface::GetPtr(MODULE("datacache"), "VDataCache003"); - } - return dataCache; -} - -bool ParseMiB(const char *text, int &out) { - char *end = nullptr; - errno = 0; - auto value = std::strtol(text, &end, 10); - if (errno || end == text || *end || value <= 0 || value > INT_MAX / BYTES_PER_MIB) { - return false; - } - out = static_cast(value); - return true; -} - -bool IsIdleMenuState() { - if (!engine || !engine->hoststate || !engine->m_szLevelName || !engine->demoplayer) return false; - - auto hoststateRun = HS_RUN; - if (sar.game->Is(SourceGame_INFRA)) hoststateRun = INFRA_HS_RUN; - - return engine->hoststate->m_currentState == hoststateRun - && !engine->hoststate->m_activeGame - && !engine->demoplayer->IsPlaying() - && std::strlen(engine->m_szLevelName) == 0 - && engine->GetMaxClients() <= 1; -} - -const char *FlushCommandForMode(int mode) { - return mode >= 2 ? "flush_locked" : "flush"; -} -} // namespace - -namespace DataCacheTools { -void AfterDemoStop() { - if (sar_demo_datacache_flush_between_demos.GetInt() > 0) { - g_pendingBetweenDemoFlush = true; - g_waitingForMenuFrames = 0; - g_idleMenuFrames = 0; - } -} - -bool BeforeQueuedDemoStart() { - auto mode = sar_demo_datacache_flush_between_demos.GetInt(); - if (mode <= 0) { - g_pendingBetweenDemoFlush = false; - return true; - } - - if (!g_pendingBetweenDemoFlush) return true; - if (mode >= 2 && !sar_demo_datacache_allow_unsafe_flush_locked.GetBool()) { - console->Warning("SAR: Refusing queued flush_locked; set sar_demo_datacache_allow_unsafe_flush_locked 1 to force this unsafe experiment.\n"); - g_pendingBetweenDemoFlush = false; - g_waitingForMenuFrames = 0; - g_idleMenuFrames = 0; - return true; - } - - if (!IsIdleMenuState()) { - if (g_waitingForMenuFrames == 0 || g_waitingForMenuFrames % 60 == 0) { - console->Print("Waiting for idle menu state before demo datacache flush.\n"); - } - ++g_waitingForMenuFrames; - g_idleMenuFrames = 0; - return false; - } - - auto delayFrames = sar_demo_datacache_flush_delay_frames.GetInt(); - if (g_idleMenuFrames < delayFrames) { - if (g_idleMenuFrames == 0 || g_idleMenuFrames % 60 == 0) { - console->Print("Waiting %d idle menu frames before demo datacache flush.\n", delayFrames - g_idleMenuFrames); - } - ++g_idleMenuFrames; - return false; - } - - auto command = FlushCommandForMode(mode); - console->Print("Running %s before queued demo start.\n", command); - engine->ExecuteCommand(command, true); - g_pendingBetweenDemoFlush = false; - g_waitingForMenuFrames = 0; - g_idleMenuFrames = 0; - return true; -} -} // namespace DataCacheTools - -CON_COMMAND(sar_datacache_set_size_mb, "sar_datacache_set_size_mb - directly sets VDataCache capacity, bypassing datacachesize cvar limits\n") { - if (args.ArgC() != 2) { - return console->Print(sar_datacache_set_size_mb.ThisPtr()->m_pszHelpString); - } - - int mib = 0; - if (!ParseMiB(args[1], mib)) { - return console->Print("Invalid datacache size. Use a positive integer MiB value.\n"); - } - - auto dataCache = GetDataCache(); - if (!dataCache) { - return console->Warning("SAR: Failed to get VDataCache003 from datacache.\n"); - } - - auto setSize = Memory::VMT(dataCache, DATA_CACHE_SET_SIZE_VTABLE_INDEX); - setSize(dataCache, mib * BYTES_PER_MIB); - console->Print("Set VDataCache capacity to %d MB. Run cache_print_summary to confirm.\n", mib); -} diff --git a/src/Features/DataCacheTools.hpp b/src/Features/DataCacheTools.hpp deleted file mode 100644 index 086da216..00000000 --- a/src/Features/DataCacheTools.hpp +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -namespace DataCacheTools { - void AfterDemoStop(); - bool BeforeQueuedDemoStart(); -} diff --git a/src/Features/Demo/ModelCacheTools.cpp b/src/Features/Demo/ModelCacheTools.cpp new file mode 100644 index 00000000..f45aef81 --- /dev/null +++ b/src/Features/Demo/ModelCacheTools.cpp @@ -0,0 +1,73 @@ +#include "Event.hpp" +#include "Modules/Console.hpp" +#include "Offsets.hpp" +#include "Utils.hpp" +#include "Utils/Memory.hpp" +#include "Variable.hpp" + +namespace { +constexpr unsigned int MODEL_FLAG_PROTECTED_MASK = 0x7E; +constexpr unsigned int MODEL_FLAG_SKIP_PROTECTED_CLEAR = 0x40; +constexpr unsigned int MAX_REASONABLE_MODEL_COUNT = 16384; + +Variable sar_demo_modelcache_clear_protected_flags( + "sar_demo_modelcache_clear_protected_flags", + "0", + "Fix demo model-cache growth by clearing stale CModelLoader protected flags on demo stop.\n", + FCVAR_CHEAT); + +void *GetModelLoader() { + static uintptr_t global = 0; + if (!global) { + auto site = Memory::Scan(MODULE("engine"), Offsets::CModelLoaderModelPrecache); + if (!site) return nullptr; + + global = Memory::Deref(site + Offsets::CModelLoaderModelPrecacheGlobal); + } + + return Memory::Deref(global); +} + +void ClearProtectedModelFlags() { + auto modelLoader = reinterpret_cast(GetModelLoader()); + if (!modelLoader) { + static bool warned = false; + if (!warned) { + warned = true; + console->Warning("SAR: Failed to find CModelLoader for repeated-demo model-cache cleanup.\n"); + } + return; + } + + auto entries = *reinterpret_cast(modelLoader + Offsets::CModelLoaderEntryArray); + auto count = *reinterpret_cast(modelLoader + Offsets::CModelLoaderEntryCount); + if (!entries || count > MAX_REASONABLE_MODEL_COUNT) { + static bool warned = false; + if (!warned) { + warned = true; + console->Warning( + "SAR: Invalid CModelLoader state (loader=%p entries=%p count=%u).\n", + reinterpret_cast(modelLoader), + reinterpret_cast(entries), + count); + } + return; + } + + for (unsigned int i = 0; i < count; ++i) { + auto model = *reinterpret_cast(entries + i * Offsets::CModelLoaderEntryStride + Offsets::CModelLoaderEntryModel); + if (!model) continue; + + auto flags = reinterpret_cast(model + Offsets::CModelLoaderModelFlags); + if ((*flags & MODEL_FLAG_SKIP_PROTECTED_CLEAR) != 0) continue; + + *flags &= ~MODEL_FLAG_PROTECTED_MASK; + } +} +} + +ON_EVENT(DEMO_STOP) { + if (sar_demo_modelcache_clear_protected_flags.GetBool()) { + ClearProtectedModelFlags(); + } +} diff --git a/src/Features/ModelCacheTools.cpp b/src/Features/ModelCacheTools.cpp deleted file mode 100644 index a3a8f925..00000000 --- a/src/Features/ModelCacheTools.cpp +++ /dev/null @@ -1,435 +0,0 @@ -#include "ModelCacheTools.hpp" - -#include "Command.hpp" -#include "Modules/Console.hpp" -#include "Utils.hpp" -#include "Utils/Memory.hpp" -#include "Variable.hpp" - -namespace { -constexpr int CMODELLOADER_FULL_RESET_VTABLE_INDEX = 1; -constexpr int CMODELLOADER_STRONG_CLEANUP_VTABLE_INDEX = 12; -constexpr int CMODELLOADER_ENTRY_ARRAY_OFFSET = 0x8; -constexpr int CMODELLOADER_ENTRY_COUNT_OFFSET = 0x16; -constexpr int CMODELLOADER_ENTRY_STRIDE = 0x10; -constexpr int CMODELLOADER_ENTRY_MODEL_OFFSET = 0xC; -constexpr int MODEL_FLAGS_OFFSET = 0x108; -constexpr unsigned int MODEL_FLAG_PROTECTED_MASK = 0x7E; -constexpr unsigned int MODEL_FLAG_PROTECTED_PRELOAD = 0x4; -constexpr unsigned int MODEL_FLAG_SKIP_PROTECTED_CLEAR = 0x40; -constexpr unsigned int MAX_REASONABLE_MODEL_COUNT = 16384; - -using CModelLoaderStrongCleanupFn = void(__rescall *)(void *thisptr); -using CModelLoaderFullResetFn = void(__rescall *)(void *thisptr); - -Variable sar_demo_modelcache_cleanup( - "sar_demo_modelcache_cleanup", - "0", - 0, - 3, - "Experimental repeated-demo cleanup. 0=off, 1=after demo stop, 2=before next demo start, 3=both.\n"); -Variable sar_demo_modelcache_unprotect_models( - "sar_demo_modelcache_unprotect_models", - "0", - "Experimental repeated-demo patch. During demo playback, load client model precache/consistency models with flag 0 instead of protected flag 4.\n"); -Variable sar_demo_modelcache_clear_flag4( - "sar_demo_modelcache_clear_flag4", - "0", - 0, - 3, - "Experimental repeated-demo cleanup. 0=off, 1=after demo stop, 2=before next demo start, 3=both. Clears CModelLoader model flag 4; automatic strong cleanup is skipped unless sar_demo_modelcache_cleanup_after_clear is set.\n"); -Variable sar_demo_modelcache_clear_protected_flags( - "sar_demo_modelcache_clear_protected_flags", - "0", - 0, - 3, - "Experimental repeated-demo cleanup. 0=off, 1=after demo stop, 2=before next demo start, 3=both. Clears CModelLoader protected flag mask 0x7E without unloading models; models with bit 0x40 are skipped.\n"); -Variable sar_demo_modelcache_cleanup_after_clear( - "sar_demo_modelcache_cleanup_after_clear", - "0", - "Unsafe experiment. If set, automatic strong cleanup is allowed to run while CModelLoader protected-flag clearing is enabled.\n"); -Variable sar_demo_modelcache_full_reset( - "sar_demo_modelcache_full_reset", - "0", - 0, - 3, - "Experimental repeated-demo CModelLoader full reset. 0=off, 1=after demo stop, 2=before next demo start, 3=both.\n"); -Variable sar_demo_modelcache_report( - "sar_demo_modelcache_report", - "0", - 0, - 3, - "Repeated-demo diagnostic. 0=off, 1=after demo stop, 2=before next demo start, 3=both. Reports CModelLoader model flag counts without changing state.\n"); - -bool g_cleanupPendingBeforeStart = false; -Memory::Patch *g_modelPrecacheFlagPatch = nullptr; -Memory::Patch *g_modelConsistencyFlagPatch = nullptr; - -bool IsAddressInModule(uintptr_t address, const char *moduleName) { - Memory::ModuleInfo info = {}; - if (!Memory::TryGetModule(moduleName, &info)) return false; - - return address >= info.base && address < info.base + info.size; -} - -uintptr_t ReadEngineGlobalFromPattern(uintptr_t site, int immediateOffset, const char *name) { - if (!site) return 0; - - auto candidate = *reinterpret_cast(site + immediateOffset); - if (!IsAddressInModule(candidate, MODULE("engine"))) { - console->Warning("SAR: Ignoring suspicious %s global address %p.\n", name, reinterpret_cast(candidate)); - return 0; - } - - return candidate; -} - -uintptr_t FindModelLoaderGlobal() { -#ifdef _WIN32 - static uintptr_t global = 0; - if (global) return global; - - // SV_ActivateServer-specific context for: mov ecx, g_pModelLoader; vtable +0x30. - uintptr_t site = Memory::Scan(MODULE("engine"), "80 3D ? ? ? ? 00 74 0D 8B 0D ? ? ? ? 8B 01 8B 50 30 FF D2 83 3D ? ? ? ? 01 7F"); - global = ReadEngineGlobalFromPattern(site, 11, "SV_ActivateServer CModelLoader"); - if (global) return global; - - // Client modelprecache-specific context for: g_pModelLoader->FindOrLoadModel(name, 4). - site = Memory::Scan(MODULE("engine"), "8B 0D ? ? ? ? 8B 11 6A 04 50 8B 42 1C FF D0 50 EB 02 6A 00"); - global = ReadEngineGlobalFromPattern(site, 2, "modelprecache CModelLoader"); - if (global) return global; - - // Client consistency-check model path uses the same global. - site = Memory::Scan(MODULE("engine"), "8B 0D ? ? ? ? 8B 01 8B 50 1C 6A 04 53 FF D2 8B F8"); - global = ReadEngineGlobalFromPattern(site, 2, "consistency CModelLoader"); - if (global) return global; - - return 0; -#else - return 0; -#endif -} - -void *GetModelLoader() { - auto global = FindModelLoaderGlobal(); - if (!global) return nullptr; - - return *reinterpret_cast(global); -} - -bool GetModelTable(uintptr_t &modelLoader, uintptr_t &entries, unsigned short &count, const char *action) { - modelLoader = reinterpret_cast(GetModelLoader()); - if (!modelLoader) { - console->Warning("SAR: Failed to find CModelLoader for %s.\n", action); - return false; - } - - entries = *reinterpret_cast(modelLoader + CMODELLOADER_ENTRY_ARRAY_OFFSET); - count = *reinterpret_cast(modelLoader + CMODELLOADER_ENTRY_COUNT_OFFSET); - if (!entries || count > MAX_REASONABLE_MODEL_COUNT) { - console->Warning( - "SAR: Refusing CModelLoader %s with suspicious state (loader=%p entries=%p count=%u).\n", - action, - reinterpret_cast(modelLoader), - reinterpret_cast(entries), - count); - return false; - } - - return true; -} - -bool RunStrongCleanup(const char *reason, bool verbose) { - auto modelLoader = GetModelLoader(); - if (!modelLoader) { - if (verbose) console->Warning("SAR: Failed to find CModelLoader for model-cache cleanup.\n"); - return false; - } - - auto cleanup = Memory::VMT(modelLoader, CMODELLOADER_STRONG_CLEANUP_VTABLE_INDEX); - if (!cleanup) { - if (verbose) console->Warning("SAR: Failed to find CModelLoader strong cleanup method.\n"); - return false; - } - - cleanup(modelLoader); - if (verbose) console->Print("Ran CModelLoader strong cleanup (%s).\n", reason); - return true; -} - -bool RunFullReset(const char *reason, bool verbose) { - auto modelLoader = GetModelLoader(); - if (!modelLoader) { - if (verbose) console->Warning("SAR: Failed to find CModelLoader for model-cache full reset.\n"); - return false; - } - - auto fullReset = Memory::VMT(modelLoader, CMODELLOADER_FULL_RESET_VTABLE_INDEX); - if (!fullReset) { - if (verbose) console->Warning("SAR: Failed to find CModelLoader full reset method.\n"); - return false; - } - - fullReset(modelLoader); - if (verbose) console->Print("Ran CModelLoader full reset (%s).\n", reason); - return true; -} - -bool ShouldCleanup(int bit) { - return (sar_demo_modelcache_cleanup.GetInt() & bit) != 0; -} - -bool ShouldClearFlag4(int bit) { - return (sar_demo_modelcache_clear_flag4.GetInt() & bit) != 0; -} - -bool ShouldClearProtectedFlags(int bit) { - return (sar_demo_modelcache_clear_protected_flags.GetInt() & bit) != 0; -} - -bool ShouldFullReset(int bit) { - return (sar_demo_modelcache_full_reset.GetInt() & bit) != 0; -} - -bool ShouldReport(int bit) { - return (sar_demo_modelcache_report.GetInt() & bit) != 0; -} - -bool RunAutomaticStrongCleanup(const char *reason) { - if ((sar_demo_modelcache_clear_flag4.GetInt() || sar_demo_modelcache_clear_protected_flags.GetInt()) - && !sar_demo_modelcache_cleanup_after_clear.GetBool()) { - console->Print("Skipping CModelLoader strong cleanup (%s) because protected-flag clearing is active.\n", reason); - return false; - } - - return RunStrongCleanup(reason, true); -} - -int ClearModelFlag4(const char *reason, bool verbose) { - uintptr_t modelLoader = 0; - uintptr_t entries = 0; - unsigned short count = 0; - if (!GetModelTable(modelLoader, entries, count, "model flag cleanup")) return 0; - - if (verbose) { - console->Print( - "Scanning CModelLoader flag 4 (%s, loader=%p entries=%p count=%u).\n", - reason, - reinterpret_cast(modelLoader), - reinterpret_cast(entries), - count); - } - - int cleared = 0; - for (unsigned int i = 0; i < count; ++i) { - auto model = *reinterpret_cast(entries + i * CMODELLOADER_ENTRY_STRIDE + CMODELLOADER_ENTRY_MODEL_OFFSET); - if (!model) continue; - - auto flags = reinterpret_cast(model + MODEL_FLAGS_OFFSET); - if ((*flags & MODEL_FLAG_PROTECTED_PRELOAD) == 0) continue; - - *flags &= ~MODEL_FLAG_PROTECTED_PRELOAD; - ++cleared; - } - - if (verbose) console->Print("Cleared CModelLoader flag 4 from %d models (%s).\n", cleared, reason); - return cleared; -} - -int ClearModelProtectedFlags(const char *reason, bool verbose) { - uintptr_t modelLoader = 0; - uintptr_t entries = 0; - unsigned short count = 0; - if (!GetModelTable(modelLoader, entries, count, "model protected-flag cleanup")) return 0; - - if (verbose) { - console->Print( - "Scanning CModelLoader protected flags (%s, loader=%p entries=%p count=%u).\n", - reason, - reinterpret_cast(modelLoader), - reinterpret_cast(entries), - count); - } - - int cleared = 0; - int skippedBit40 = 0; - for (unsigned int i = 0; i < count; ++i) { - auto model = *reinterpret_cast(entries + i * CMODELLOADER_ENTRY_STRIDE + CMODELLOADER_ENTRY_MODEL_OFFSET); - if (!model) continue; - - auto flags = reinterpret_cast(model + MODEL_FLAGS_OFFSET); - if ((*flags & MODEL_FLAG_SKIP_PROTECTED_CLEAR) != 0) { - ++skippedBit40; - continue; - } - - if ((*flags & MODEL_FLAG_PROTECTED_MASK) == 0) continue; - *flags &= ~MODEL_FLAG_PROTECTED_MASK; - ++cleared; - } - - if (verbose) { - console->Print( - "Cleared CModelLoader protected flags 0x7E from %d models; skipped %d bit0x40 models (%s).\n", - cleared, - skippedBit40, - reason); - } - return cleared; -} - -void ReportModelFlags(const char *reason) { - uintptr_t modelLoader = 0; - uintptr_t entries = 0; - unsigned short count = 0; - if (!GetModelTable(modelLoader, entries, count, "model flag report")) return; - - unsigned int models = 0; - unsigned int zero = 0; - unsigned int protectedFlags = 0; - unsigned int bit2 = 0; - unsigned int bit4 = 0; - unsigned int bit8 = 0; - unsigned int bit10 = 0; - unsigned int bit20 = 0; - unsigned int bit40 = 0; - - for (unsigned int i = 0; i < count; ++i) { - auto model = *reinterpret_cast(entries + i * CMODELLOADER_ENTRY_STRIDE + CMODELLOADER_ENTRY_MODEL_OFFSET); - if (!model) continue; - - ++models; - auto flags = *reinterpret_cast(model + MODEL_FLAGS_OFFSET); - if (flags == 0) ++zero; - if (flags & MODEL_FLAG_PROTECTED_MASK) ++protectedFlags; - if (flags & 0x2) ++bit2; - if (flags & 0x4) ++bit4; - if (flags & 0x8) ++bit8; - if (flags & 0x10) ++bit10; - if (flags & 0x20) ++bit20; - if (flags & 0x40) ++bit40; - } - - console->Print( - "CModelLoader report (%s): loader=%p entries=%p count=%u models=%u.\n", - reason, - reinterpret_cast(modelLoader), - reinterpret_cast(entries), - count, - models); - console->Print( - "CModelLoader flags (%s): zero=%u protected0x7e=%u bit0x02=%u bit0x04=%u bit0x08=%u bit0x10=%u bit0x20=%u bit0x40=%u.\n", - reason, - zero, - protectedFlags, - bit2, - bit4, - bit8, - bit10, - bit20, - bit40); -} - -bool InitFlagPatch(Memory::Patch *&patch, const char *pattern, int immediateOffset, const char *name) { - if (!patch) patch = new Memory::Patch(); - if (patch->IsInit()) return true; - -#ifdef _WIN32 - auto site = Memory::Scan(MODULE("engine"), pattern); - if (!site) { - console->Warning("SAR: Failed to find %s model flag patch site.\n", name); - return false; - } - - unsigned char zeroFlag = 0; - if (!patch->Execute(site + immediateOffset, &zeroFlag, 1)) { - console->Warning("SAR: Failed to apply %s model flag patch.\n", name); - return false; - } - patch->Restore(); - return true; -#else - return false; -#endif -} - -void SetFlagPatches(bool enabled) { - if (!sar_demo_modelcache_unprotect_models.GetBool()) enabled = false; - if (!enabled) { - if (g_modelPrecacheFlagPatch) g_modelPrecacheFlagPatch->Restore(); - if (g_modelConsistencyFlagPatch) g_modelConsistencyFlagPatch->Restore(); - return; - } - - bool ready = InitFlagPatch( - g_modelPrecacheFlagPatch, - "8B 0D ? ? ? ? 8B 11 6A 04 50 8B 42 1C FF D0 50 EB 02 6A 00", - 9, - "modelprecache"); - ready = InitFlagPatch( - g_modelConsistencyFlagPatch, - "8B 0D ? ? ? ? 8B 01 8B 50 1C 6A 04 53 FF D2 8B F8", - 12, - "consistency") && ready; - if (!ready) return; - - g_modelPrecacheFlagPatch->Execute(); - g_modelConsistencyFlagPatch->Execute(); - console->Print("Patched demo model loads to use unprotected flag 0.\n"); -} -} // namespace - -namespace ModelCacheTools { -void CleanupAfterDemoStop() { - g_cleanupPendingBeforeStart = true; - bool report = ShouldReport(1); - bool mutating = ShouldFullReset(1) || ShouldClearProtectedFlags(1) || ShouldClearFlag4(1) || ShouldCleanup(1); - if (report && mutating) ReportModelFlags("after demo stop before cleanup"); - if (ShouldFullReset(1)) RunFullReset("after demo stop", true); - if (ShouldClearProtectedFlags(1)) ClearModelProtectedFlags("after demo stop", true); - if (ShouldClearFlag4(1)) ClearModelFlag4("after demo stop", true); - if (ShouldCleanup(1)) { - RunAutomaticStrongCleanup("after demo stop"); - } - if (report) ReportModelFlags(mutating ? "after demo stop after cleanup" : "after demo stop"); - SetFlagPatches(false); -} - -void CleanupBeforeDemoStart() { - if (g_cleanupPendingBeforeStart) { - g_cleanupPendingBeforeStart = false; - bool report = ShouldReport(2); - bool mutating = ShouldFullReset(2) || ShouldClearProtectedFlags(2) || ShouldClearFlag4(2) || ShouldCleanup(2); - if (report && mutating) ReportModelFlags("before demo start before cleanup"); - if (ShouldFullReset(2)) RunFullReset("before demo start", true); - if (ShouldClearProtectedFlags(2)) ClearModelProtectedFlags("before demo start", true); - if (ShouldClearFlag4(2)) ClearModelFlag4("before demo start", true); - - if (ShouldCleanup(2)) { - RunAutomaticStrongCleanup("before demo start"); - } - if (report) ReportModelFlags(mutating ? "before demo start after cleanup" : "before demo start"); - } - SetFlagPatches(true); -} -} // namespace ModelCacheTools - -CON_COMMAND(sar_modelcache_strong_cleanup, "sar_modelcache_strong_cleanup - manually runs CModelLoader's stronger stale-flag cleanup\n") { - RunStrongCleanup("manual command", true); -} - -CON_COMMAND(sar_modelcache_full_reset, "sar_modelcache_full_reset - manually runs CModelLoader's full reset/unload path\n") { - RunFullReset("manual command", true); -} - -CON_COMMAND(sar_modelcache_clear_flag4, "sar_modelcache_clear_flag4 - manually clears CModelLoader model flag 4 without running stronger cleanup\n") { - ClearModelFlag4("manual command", true); -} - -CON_COMMAND(sar_modelcache_clear_protected_flags, "sar_modelcache_clear_protected_flags - manually clears CModelLoader protected flags 0x7E without unloading models\n") { - ClearModelProtectedFlags("manual command", true); -} - -CON_COMMAND(sar_modelcache_report, "sar_modelcache_report - reports CModelLoader model flag counts without changing state\n") { - ReportModelFlags("manual command"); -} diff --git a/src/Features/ModelCacheTools.hpp b/src/Features/ModelCacheTools.hpp deleted file mode 100644 index eee844d3..00000000 --- a/src/Features/ModelCacheTools.hpp +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -namespace ModelCacheTools { - void CleanupAfterDemoStop(); - void CleanupBeforeDemoStart(); -} diff --git a/src/Modules/Engine.cpp b/src/Modules/Engine.cpp index b809aeaf..2a38e91d 100644 --- a/src/Modules/Engine.cpp +++ b/src/Modules/Engine.cpp @@ -10,7 +10,6 @@ #include "Features/AchievementTracker.hpp" #include "Features/Camera.hpp" #include "Features/Cvars.hpp" -#include "Features/DataCacheTools.hpp" #include "Features/Demo/DemoParser.hpp" #include "Features/Demo/NetworkGhostPlayer.hpp" #include "Features/Hud/PerformanceHud.hpp" @@ -404,13 +403,11 @@ DETOUR(Engine::Frame) { //demoplayer if (engine->demoplayer->demoQueueSize > 0 && !engine->demoplayer->IsPlaying() && engine->demoplayer->IsPlaybackFixReady()) { - if (DataCacheTools::BeforeQueuedDemoStart()) { - DemoParser parser; - auto name = engine->demoplayer->demoQueue[engine->demoplayer->currentDemoID]; - engine->ExecuteCommand(Utils::ssprintf("playdemo \"%s\"", name.c_str()).c_str(), true); - if (++engine->demoplayer->currentDemoID >= engine->demoplayer->demoQueueSize) { - engine->demoplayer->ClearDemoQueue(); - } + DemoParser parser; + auto name = engine->demoplayer->demoQueue[engine->demoplayer->currentDemoID]; + engine->ExecuteCommand(Utils::ssprintf("playdemo \"%s\"", name.c_str()).c_str(), true); + if (++engine->demoplayer->currentDemoID >= engine->demoplayer->demoQueueSize) { + engine->demoplayer->ClearDemoQueue(); } } diff --git a/src/Modules/EngineDemoPlayer.cpp b/src/Modules/EngineDemoPlayer.cpp index 17442313..86987ede 100644 --- a/src/Modules/EngineDemoPlayer.cpp +++ b/src/Modules/EngineDemoPlayer.cpp @@ -6,10 +6,8 @@ #include "Engine.hpp" #include "Event.hpp" #include "Features/Camera.hpp" -#include "Features/DataCacheTools.hpp" #include "Features/Demo/Demo.hpp" #include "Features/Demo/DemoParser.hpp" -#include "Features/ModelCacheTools.hpp" #include "Features/Renderer.hpp" #include "Hook.hpp" #include "Interface.hpp" @@ -237,8 +235,6 @@ DETOUR_COMMAND(EngineDemoPlayer::stopdemo) { // CDemoPlayer::StartPlayback DETOUR(EngineDemoPlayer::StartPlayback, const char *filename, bool bAsTimeDemo) { - ModelCacheTools::CleanupBeforeDemoStart(); - auto path = std::string(filename); path = fileSystem->FindFileSomewhere(path).value_or(path); auto newFilename = path.c_str(); @@ -280,17 +276,10 @@ DETOUR(EngineDemoPlayer::StartPlayback, const char *filename, bool bAsTimeDemo) // CDemoPlayer::StopPlayback DETOUR(EngineDemoPlayer::StopPlayback) { - bool stoppedDemo = engine->demoplayer->IsPlaying() && !g_demoFixing; - if (stoppedDemo) { + if (engine->demoplayer->IsPlaying() && !g_demoFixing) { Event::Trigger({}); } - - auto result = EngineDemoPlayer::StopPlayback(thisptr); - if (stoppedDemo) { - ModelCacheTools::CleanupAfterDemoStop(); - DataCacheTools::AfterDemoStop(); - } - return result; + return EngineDemoPlayer::StopPlayback(thisptr); } Variable sar_demo_portal_interp_fix("sar_demo_portal_interp_fix", "1", "Fix eye interpolation through portals in demo playback.\n"); diff --git a/src/Offsets/Portal 2 9568.hpp b/src/Offsets/Portal 2 9568.hpp index 820e0826..be38ef68 100644 --- a/src/Offsets/Portal 2 9568.hpp +++ b/src/Offsets/Portal 2 9568.hpp @@ -481,7 +481,21 @@ OFFSET_DEFAULT(StartupDemoFile_HeaderName, 212, 184) // EngineDemoPlayer SIGSCAN_DEFAULT(InterpolateDemoCommand, "55 8B EC 83 EC 10 56 8B F1 8B 4D 10 57 8B BE B4 05 00 00 83 C1 04 89 75 F4 89 7D F0 E8 ? ? ? ? 8B 4D 14 83 C1 04", - "55 57 56 53 83 EC 10 8B 44 24 24 8B 5C 24 2C 8B 88 B0 05 00 00 8B 44 24 30 8D 70 04 8D 90 9C 00 00 00 89 F0 F3 0F 10 40 04") + "55 57 56 53 83 EC 10 8B 44 24 24 8B 5C 24 2C 8B 88 B0 05 00 00 8B 44 24 30 8D 70 04 8D 90 9C 00 00 00 89 F0 F3 0F 10 40 04") + + +// CModelLoader +// win: "modelprecache" xref -> client string-table update callback -> CModelLoader vtable +0x1C call with flag 4; global immediate is g_pModelLoader +// linux: "CClientState::ConsistencyCheck" xref -> model consistency type 3 block -> CModelLoader vtable +0x1C call with flag 4; global immediate is g_pModelLoader +SIGSCAN_DEFAULT(CModelLoaderModelPrecache, + "8B 0D ? ? ? ? 8B 11 6A 04 50 8B 42 1C FF D0 50 EB 02 6A 00", + "A1 ? ? ? ? 83 EC 04 8B 10 6A 04 FF B5 ? ? ? ? 50 FF 52 1C 89 85 ? ? ? ? 83 C4 10 85 C0") +OFFSET_DEFAULT(CModelLoaderModelPrecacheGlobal, 2, 1) +OFFSET_DEFAULT(CModelLoaderEntryArray, 0x8, 0x8) // "CModelLoader::FindModel: NULL name" xref -> successful lookup path reads [this+8] + index*0x10 + 0xC +OFFSET_DEFAULT(CModelLoaderEntryCount, 0x16, 0x16) // same CModelLoader::FindModel tree/list state; active count is this+0x16 +OFFSET_DEFAULT(CModelLoaderEntryStride, 0x10, 0x10) // same CModelLoader::FindModel lookup path; entry nodes are 0x10 bytes +OFFSET_DEFAULT(CModelLoaderEntryModel, 0xC, 0xC) // same CModelLoader::FindModel lookup path; entry+0xC is model_t * +OFFSET_DEFAULT(CModelLoaderModelFlags, 0x108, 0x108) // CModelLoader vtable +0x1C target ORs caller flags into model_t+0x108 // Matchmaking From cfefc86b18327c58608413b5b68877a74e57ad26 Mon Sep 17 00:00:00 2001 From: Betsruner <55611319+betsruner@users.noreply.github.com> Date: Wed, 27 May 2026 21:19:21 -0500 Subject: [PATCH 3/3] docs fix, offsets fix, cheats protection fix --- docs/cvars.md | 1 + src/Features/Demo/ModelCacheTools.cpp | 17 ++++++++++++++--- src/Offsets/Portal 2 5723.hpp | 9 +++++++++ src/Offsets/Portal 2 8151.hpp | 17 +++++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/docs/cvars.md b/docs/cvars.md index beeca5d8..63c9e43d 100644 --- a/docs/cvars.md +++ b/docs/cvars.md @@ -173,6 +173,7 @@ |sar_demo_clean_start|0|Attempts to minimize visual interpolation of some elements (like post-processing or lighting) when demo playback begins.| |sar_demo_clean_start_tonemap|0|Overrides initial tonemap scalar value used in auto-exposure.
Setting it to 0 will attempt to skip over to target value for several ticks.| |sar_demo_clean_start_tonemap_sample|cmd|sar_demo_clean_start_tonemap_sample [tick] - samples tonemap scale from current demo at given tick and stores it in "sar_demo_clean_start_tonemap" variable. If no tick is given, sampling will happen when `__END__` is seen in demo playback.| +|sar_demo_modelcache_clear_protected_flags|0|Fix demo model-cache growth by clearing stale CModelLoader protected flags on demo stop.| |sar_demo_overwrite_bak|0|Rename demos to (name)_bak if they would be overwritten by recording| |sar_demo_portal_interp_fix|1|Fix eye interpolation through portals in demo playback.| |sar_demo_remove_broken|1|Whether to remove broken frames from demo playback| diff --git a/src/Features/Demo/ModelCacheTools.cpp b/src/Features/Demo/ModelCacheTools.cpp index f45aef81..2b90a45d 100644 --- a/src/Features/Demo/ModelCacheTools.cpp +++ b/src/Features/Demo/ModelCacheTools.cpp @@ -1,5 +1,6 @@ #include "Event.hpp" #include "Modules/Console.hpp" +#include "Modules/Server.hpp" #include "Offsets.hpp" #include "Utils.hpp" #include "Utils/Memory.hpp" @@ -10,11 +11,21 @@ constexpr unsigned int MODEL_FLAG_PROTECTED_MASK = 0x7E; constexpr unsigned int MODEL_FLAG_SKIP_PROTECTED_CLEAR = 0x40; constexpr unsigned int MAX_REASONABLE_MODEL_COUNT = 16384; +DECL_CVAR_CALLBACK(sar_demo_modelcache_clear_protected_flags); + Variable sar_demo_modelcache_clear_protected_flags( "sar_demo_modelcache_clear_protected_flags", "0", - "Fix demo model-cache growth by clearing stale CModelLoader protected flags on demo stop.\n", - FCVAR_CHEAT); + "Fix demo model-cache growth by clearing stale CModelLoader protected flags on demo stop. Requires sv_cheats 1 to enable.\n", + FCVAR_NONE, + sar_demo_modelcache_clear_protected_flags_callback); + +DECL_CVAR_CALLBACK(sar_demo_modelcache_clear_protected_flags) { + if (sar_demo_modelcache_clear_protected_flags.GetBool() && flOldValue == 0.0f && !sv_cheats.GetBool()) { + console->Print("sar_demo_modelcache_clear_protected_flags requires sv_cheats 1.\n"); + sar_demo_modelcache_clear_protected_flags.SetValue(pOldValue ? pOldValue : "0"); + } +} void *GetModelLoader() { static uintptr_t global = 0; @@ -64,7 +75,7 @@ void ClearProtectedModelFlags() { *flags &= ~MODEL_FLAG_PROTECTED_MASK; } } -} +} // namespace ON_EVENT(DEMO_STOP) { if (sar_demo_modelcache_clear_protected_flags.GetBool()) { diff --git a/src/Offsets/Portal 2 5723.hpp b/src/Offsets/Portal 2 5723.hpp index bbab9f47..746730b3 100644 --- a/src/Offsets/Portal 2 5723.hpp +++ b/src/Offsets/Portal 2 5723.hpp @@ -30,3 +30,12 @@ OFFSET_LINUX(DrawPortalSpBranchOff, 0x15) SIGSCAN_LINUX(DrawPortalGhost, "55 89 E5 57 56 53 83 EC 5C A1 ? ? ? ? 8B 40") SIGSCAN_LINUX(DrawPortalGhostSpBranch, "0F 84 ? ? ? ? FF 90 ? ? ? ? 80 BB ? ? ? ? 01") SIGSCAN_LINUX(GetChapterProgress, "55 89 E5 57 56 53 83 EC 2C 8B 7D 08 E8 ? ? ? ? 8B 10 C7") +SIGSCAN_LINUX(DispatchParticleEffect,"") +SIGSCAN_LINUX(PrecacheParticleSystem, "") +SIGSCAN_LINUX(GetCurrentTonemappingSystem, "") +SIGSCAN_LINUX(ResetToneMapping, "") +SIGSCAN_LINUX(LoadingProgress__SetupControlStatesInstruction, "") + +// Server +SIGSCAN_LINUX(FloorReportalBranch,"") +SIGSCAN_LINUX(CPortal_Player__PollForUseEntity_CheckMP, "") \ No newline at end of file diff --git a/src/Offsets/Portal 2 8151.hpp b/src/Offsets/Portal 2 8151.hpp index f79461e8..796e189e 100644 --- a/src/Offsets/Portal 2 8151.hpp +++ b/src/Offsets/Portal 2 8151.hpp @@ -68,6 +68,11 @@ SIGSCAN_LINUX(AddShadowToReceiver, "55 89 E5 57 56 53 83 EC ? 8B 45 ? 8B 4D ? 8B SIGSCAN_LINUX(UTIL_Portal_Color, "55 89 E5 56 53 83 EC 10 8B 75 ? 8B 5D ? 85 F6 0F 84") SIGSCAN_LINUX(UTIL_Portal_Color_Particles, "55 89 E5 53 83 EC 14 A1 ? ? ? ? 8B 5D ? 8B 10 89 04 24 FF 92 ? ? ? ? 84 C0 75 ? 83 7D ? 01") SIGSCAN_LINUX(GetChapterProgress, "55 89 E5 57 56 53 83 EC 2C 8B 5D 08 E8 ? ? ? ? 8B 10") +SIGSCAN_LINUX(DispatchParticleEffect, "") +SIGSCAN_LINUX(PrecacheParticleSystem, "") +SIGSCAN_LINUX(GetCurrentTonemappingSystem, "") +SIGSCAN_LINUX(ResetToneMapping, "") +SIGSCAN_LINUX(LoadingProgress__SetupControlStatesInstruction, "") // Engine SIGSCAN_LINUX(Host_AccumulateTime, "55 89 E5 83 EC 28 F3 0F 10 05 ? ? ? ? A1 ? ? ? ? F3 0F 58 45 08 F3 0F 11 05 ? ? ? ? 8B 10 89 04 24 FF 52 24") @@ -82,6 +87,15 @@ SIGSCAN_LINUX(InsertCommand, "55 89 E5 57 56 53 83 EC 1C 8B 75 ? 8B 5D ? 81 FE F // EngineDemoPlayer SIGSCAN_LINUX(InterpolateDemoCommand, "55 31 C9 89 E5 57 56 53 83 EC 3C 89 4D F0 8B 45 08 8B 4D 14 8B 80 B0 05 00 00 89 45 B8 8B 45 14 83 C0 04 89 45 D0") +// CModelLoader +SIGSCAN_LINUX(CModelLoaderModelPrecache, "A1 ? ? ? ? 8B 8D ? ? ? ? 8B 10 C7 44 24 08 04 00 00 00 89 4C 24 04 89 04 24 FF 52 1C") +OFFSET_LINUX(CModelLoaderModelPrecacheGlobal, 1) +OFFSET_LINUX(CModelLoaderEntryArray, 0x8) +OFFSET_LINUX(CModelLoaderEntryCount, 0x16) +OFFSET_LINUX(CModelLoaderEntryStride, 0x10) +OFFSET_LINUX(CModelLoaderEntryModel, 0xC) +OFFSET_LINUX(CModelLoaderModelFlags, 0x108) + // MaterialSystem SIGSCAN_LINUX(KeyValues_SetString, "55 89 E5 53 83 EC ? 8B 45 ? C7 44 24 ? ? ? ? ? 8B 5D ? 89 44 24 ? 8B 45 ? 89 04 24 E8 ? ? ? ? 85 C0 74 ? 89 5D") @@ -98,5 +112,8 @@ SIGSCAN_LINUX(UTIL_GetCommandClientIndex, "A1 ? ? ? ? 55 89 E5 5D 83 C0 01 C3") SIGSCAN_LINUX(CheckStuck_FloatTime, "E8 ? ? ? ? 8B 43 04 DD 9D ? ? ? ? F2 0F 10 B5 ? ? ? ? 8B 50 24 66 0F 14 F6 66 0F 5A CE 85 D2") SIGSCAN_DEFAULT(aircontrol_fling_speedSig, "0F 2F 25 ? ? ? ? 0F 28 F0", "0F 2E 05 ? ? ? ? 0F 86 ? ? ? ? 0F 2E 25") +SIGSCAN_DEFAULT(Portal2PromoFlagsSig, "", "") +SIGSCAN_LINUX(FloorReportalBranch, "") +SIGSCAN_LINUX(CPortal_Player__PollForUseEntity_CheckMP, "") // clang-format on