diff --git a/README.md b/README.md index a29a70f6..2102ab6d 100644 --- a/README.md +++ b/README.md @@ -32,18 +32,19 @@ For features and installation instructions, please visit [the website]. ## Support -| Game | Windows | Linux | -|-------------------------------------------------------------------------|---------|-------| -| [Portal 2](https://store.steampowered.com/app/620) | ✔ | ✔ | -| [Aperture Tag](https://store.steampowered.com/app/280740) | ✔ | [➖](https://wiki.portal2.sr/Aperture_Tag#Linux) | -| [Portal Stories: Mel](https://store.steampowered.com/app/317400) | ✔ | ✔ | -| [Thinking with Time Machine](https://store.steampowered.com/app/286080) | ✔ | ✔ | -| [Portal Reloaded](https://store.steampowered.com/app/1255980) | ✔ | ✔ | -| [INFRA](https://store.steampowered.com/app/251110) | ✔ | ➖ | -| [The Beginner's Guide](https://store.steampowered.com/app/303210) | ✔ | ✔ | -| [The Stanley Parable](https://store.steampowered.com/app/221910) | ✔ | ✔ | -| [The Cleaning Game](https://store.steampowered.com/app/3281900) | ✔ | ➖ | -| Divinity Chapter 2 *(closed beta)* | ✔ | ❓ | +| Game | Windows | Linux | +|----------------------------------------------------------------------------|---------|-------| +| [Portal 2 (Steam)](https://store.steampowered.com/app/620) | ✔ | ✔ | +| [Portal 2 (4554)](https://sourceunpack.gameabusefastcomplete.com/#p2-4554) | ✔ | ➖ | +| [Aperture Tag](https://store.steampowered.com/app/280740) | ✔ | [➖](https://wiki.portal2.sr/Aperture_Tag#Linux) | +| [Portal Stories: Mel](https://store.steampowered.com/app/317400) | ✔ | ✔ | +| [Thinking with Time Machine](https://store.steampowered.com/app/286080) | ✔ | ❌ | +| [Portal Reloaded](https://store.steampowered.com/app/1255980) | ✔ | ✔ | +| [INFRA](https://store.steampowered.com/app/251110) | ✔ | ➖ | +| [The Beginner's Guide](https://store.steampowered.com/app/303210) | ✔ | ✔ | +| [The Stanley Parable](https://store.steampowered.com/app/221910) | ✔ | ✔ | +| [The Cleaning Game](https://store.steampowered.com/app/3281900) | ✔ | ➖ | +| Divinity Chapter 2 *(closed beta)* | ✔ | ❓ | If you're playing a game with no Linux support, you can use Proton to run it. diff --git a/docs/cvars.md b/docs/cvars.md index beeca5d8..9ca8b56f 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. Requires sv_cheats 1 to enable.| |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/docs/web/index.html b/docs/web/index.html index 453f6c39..3884d69d 100644 --- a/docs/web/index.html +++ b/docs/web/index.html @@ -128,10 +128,15 @@

Game support

Linux - Portal 2 + Portal 2 (Steam) ✔ ✔ + + Portal 2 (4554) + ✔ + ➖ + Aperture Tag ✔ @@ -145,7 +150,7 @@

Game support

Thinking with Time Machine ✔ - ✔ + ❌ Portal Reloaded diff --git a/src/Features/Demo/ModelCacheTools.cpp b/src/Features/Demo/ModelCacheTools.cpp new file mode 100644 index 00000000..fe45900d --- /dev/null +++ b/src/Features/Demo/ModelCacheTools.cpp @@ -0,0 +1,84 @@ +#include "Event.hpp" +#include "Modules/Console.hpp" +#include "Modules/Server.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; + +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. 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() && !sv_cheats.GetBool()) { + console->Print("sar_demo_modelcache_clear_protected_flags requires sv_cheats 1.\n"); + sar_demo_modelcache_clear_protected_flags.SetValue(0); + } +} + +void *GetModelLoader() { + static uintptr_t global = 0; + if (!global) { + auto site = Memory::Scan(MODULE("engine"), Offsets::CModelLoaderModelPrecache, Offsets::CModelLoaderModelPrecacheGlobal); + if (!site) return nullptr; + + global = Memory::Deref(site); + } + + 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; + } +} +} // namespace + +ON_EVENT(DEMO_STOP) { + if (sar_demo_modelcache_clear_protected_flags.GetBool() && sv_cheats.GetBool()) { + ClearProtectedModelFlags(); + } +} diff --git a/src/Offsets/Portal 2 8151.hpp b/src/Offsets/Portal 2 8151.hpp index aca9cecb..cab8e426 100644 --- a/src/Offsets/Portal 2 8151.hpp +++ b/src/Offsets/Portal 2 8151.hpp @@ -87,6 +87,9 @@ 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") + // 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") diff --git a/src/Offsets/Portal 2 9568.hpp b/src/Offsets/Portal 2 9568.hpp index 9317e8c1..6b90abe4 100644 --- a/src/Offsets/Portal 2 9568.hpp +++ b/src/Offsets/Portal 2 9568.hpp @@ -484,6 +484,20 @@ SIGSCAN_DEFAULT(InterpolateDemoCommand, "55 8B EC 83 EC 10 56 8B F1 8B 4D 10 57 "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 SIGSCAN_DEFAULT(UpdateLeaderboardData, "55 8B EC 83 EC 08 53 8B D9 8B 03 8B 50 08", "55 89 E5 57 56 53 83 EC 2C 8B 45 08 8B 5D 0C")