Skip to content

Commit f6a6e01

Browse files
betsrunerThisAMJ
andauthored
feat: fix vanilla crash from playing too many demos (#363)
* First working version * Cleanup * docs fix, offsets fix, cheats protection fix * refactor: improve some cheat handling and others i don't trust cvar callbacks to enforce cheat protection * fixup: cleanup * docs: add 4554 to support table and twtm is now an official x. game file modification is required to load SAR. * docs regen --------- Co-authored-by: ThisAMJ <69196954+ThisAMJ@users.noreply.github.com>
1 parent 4459cbc commit f6a6e01

6 files changed

Lines changed: 122 additions & 14 deletions

File tree

README.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,19 @@ For features and installation instructions, please visit [the website].
3232

3333
## Support
3434

35-
| Game | Windows | Linux |
36-
|-------------------------------------------------------------------------|---------|-------|
37-
| [Portal 2](https://store.steampowered.com/app/620) |||
38-
| [Aperture Tag](https://store.steampowered.com/app/280740) || [](https://wiki.portal2.sr/Aperture_Tag#Linux) |
39-
| [Portal Stories: Mel](https://store.steampowered.com/app/317400) |||
40-
| [Thinking with Time Machine](https://store.steampowered.com/app/286080) |||
41-
| [Portal Reloaded](https://store.steampowered.com/app/1255980) |||
42-
| [INFRA](https://store.steampowered.com/app/251110) |||
43-
| [The Beginner's Guide](https://store.steampowered.com/app/303210) |||
44-
| [The Stanley Parable](https://store.steampowered.com/app/221910) |||
45-
| [The Cleaning Game](https://store.steampowered.com/app/3281900) |||
46-
| Divinity Chapter 2 *(closed beta)* |||
35+
| Game | Windows | Linux |
36+
|----------------------------------------------------------------------------|---------|-------|
37+
| [Portal 2 (Steam)](https://store.steampowered.com/app/620) |||
38+
| [Portal 2 (4554)](https://sourceunpack.gameabusefastcomplete.com/#p2-4554) |||
39+
| [Aperture Tag](https://store.steampowered.com/app/280740) || [](https://wiki.portal2.sr/Aperture_Tag#Linux) |
40+
| [Portal Stories: Mel](https://store.steampowered.com/app/317400) |||
41+
| [Thinking with Time Machine](https://store.steampowered.com/app/286080) |||
42+
| [Portal Reloaded](https://store.steampowered.com/app/1255980) |||
43+
| [INFRA](https://store.steampowered.com/app/251110) |||
44+
| [The Beginner's Guide](https://store.steampowered.com/app/303210) |||
45+
| [The Stanley Parable](https://store.steampowered.com/app/221910) |||
46+
| [The Cleaning Game](https://store.steampowered.com/app/3281900) |||
47+
| Divinity Chapter 2 *(closed beta)* |||
4748

4849
If you're playing a game with no Linux support, you can use Proton to run it.
4950

docs/cvars.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@
173173
|sar_demo_clean_start|0|Attempts to minimize visual interpolation of some elements (like post-processing or lighting) when demo playback begins.|
174174
|sar_demo_clean_start_tonemap|0|Overrides initial tonemap scalar value used in auto-exposure.<br>Setting it to 0 will attempt to skip over to target value for several ticks.|
175175
|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.|
176+
|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.|
176177
|sar_demo_overwrite_bak|0|Rename demos to (name)_bak if they would be overwritten by recording|
177178
|sar_demo_portal_interp_fix|1|Fix eye interpolation through portals in demo playback.|
178179
|sar_demo_remove_broken|1|Whether to remove broken frames from demo playback|

docs/web/index.html

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,15 @@ <h1 id="support">Game support</h1>
128128
<th>Linux</th>
129129
</tr>
130130
<tr>
131-
<td><a href="https://store.steampowered.com/app/620" target="_blank">Portal 2</a></td>
131+
<td><a href="https://store.steampowered.com/app/620" target="_blank">Portal 2 (Steam)</a></td>
132132
<td></td>
133133
<td></td>
134134
</tr>
135+
<tr>
136+
<td><a href="https://store.steampowered.com/app/620" target="_blank">Portal 2 (4554)</a></td>
137+
<td></td>
138+
<td></td>
139+
</tr>
135140
<tr>
136141
<td><a href="https://store.steampowered.com/app/280740" target="_blank">Aperture Tag</a></td>
137142
<td></td>
@@ -145,7 +150,7 @@ <h1 id="support">Game support</h1>
145150
<tr>
146151
<td><a href="https://store.steampowered.com/app/286080" target="_blank">Thinking with Time Machine</a></td>
147152
<td></td>
148-
<td></td>
153+
<td></td>
149154
</tr>
150155
<tr>
151156
<td><a href="https://store.steampowered.com/app/1255980" target="_blank">Portal Reloaded</a></td>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#include "Event.hpp"
2+
#include "Modules/Console.hpp"
3+
#include "Modules/Server.hpp"
4+
#include "Offsets.hpp"
5+
#include "Utils.hpp"
6+
#include "Utils/Memory.hpp"
7+
#include "Variable.hpp"
8+
9+
namespace {
10+
constexpr unsigned int MODEL_FLAG_PROTECTED_MASK = 0x7E;
11+
constexpr unsigned int MODEL_FLAG_SKIP_PROTECTED_CLEAR = 0x40;
12+
constexpr unsigned int MAX_REASONABLE_MODEL_COUNT = 16384;
13+
14+
DECL_CVAR_CALLBACK(sar_demo_modelcache_clear_protected_flags);
15+
16+
Variable sar_demo_modelcache_clear_protected_flags(
17+
"sar_demo_modelcache_clear_protected_flags",
18+
"0",
19+
"Fix demo model-cache growth by clearing stale CModelLoader protected flags on demo stop. Requires sv_cheats 1 to enable.\n",
20+
FCVAR_NONE,
21+
sar_demo_modelcache_clear_protected_flags_callback);
22+
23+
DECL_CVAR_CALLBACK(sar_demo_modelcache_clear_protected_flags) {
24+
if (sar_demo_modelcache_clear_protected_flags.GetBool() && !sv_cheats.GetBool()) {
25+
console->Print("sar_demo_modelcache_clear_protected_flags requires sv_cheats 1.\n");
26+
sar_demo_modelcache_clear_protected_flags.SetValue(0);
27+
}
28+
}
29+
30+
void *GetModelLoader() {
31+
static uintptr_t global = 0;
32+
if (!global) {
33+
auto site = Memory::Scan(MODULE("engine"), Offsets::CModelLoaderModelPrecache, Offsets::CModelLoaderModelPrecacheGlobal);
34+
if (!site) return nullptr;
35+
36+
global = Memory::Deref<uintptr_t>(site);
37+
}
38+
39+
return Memory::Deref<void *>(global);
40+
}
41+
42+
void ClearProtectedModelFlags() {
43+
auto modelLoader = reinterpret_cast<uintptr_t>(GetModelLoader());
44+
if (!modelLoader) {
45+
static bool warned = false;
46+
if (!warned) {
47+
warned = true;
48+
console->Warning("SAR: Failed to find CModelLoader for repeated-demo model-cache cleanup.\n");
49+
}
50+
return;
51+
}
52+
53+
auto entries = *reinterpret_cast<uintptr_t *>(modelLoader + Offsets::CModelLoaderEntryArray);
54+
auto count = *reinterpret_cast<unsigned short *>(modelLoader + Offsets::CModelLoaderEntryCount);
55+
if (!entries || count > MAX_REASONABLE_MODEL_COUNT) {
56+
static bool warned = false;
57+
if (!warned) {
58+
warned = true;
59+
console->Warning(
60+
"SAR: Invalid CModelLoader state (loader=%p entries=%p count=%u).\n",
61+
reinterpret_cast<void *>(modelLoader),
62+
reinterpret_cast<void *>(entries),
63+
count);
64+
}
65+
return;
66+
}
67+
68+
for (unsigned int i = 0; i < count; ++i) {
69+
auto model = *reinterpret_cast<uintptr_t *>(entries + i * Offsets::CModelLoaderEntryStride + Offsets::CModelLoaderEntryModel);
70+
if (!model) continue;
71+
72+
auto flags = reinterpret_cast<unsigned int *>(model + Offsets::CModelLoaderModelFlags);
73+
if ((*flags & MODEL_FLAG_SKIP_PROTECTED_CLEAR) != 0) continue;
74+
75+
*flags &= ~MODEL_FLAG_PROTECTED_MASK;
76+
}
77+
}
78+
} // namespace
79+
80+
ON_EVENT(DEMO_STOP) {
81+
if (sar_demo_modelcache_clear_protected_flags.GetBool() && sv_cheats.GetBool()) {
82+
ClearProtectedModelFlags();
83+
}
84+
}

src/Offsets/Portal 2 8151.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ SIGSCAN_LINUX(InsertCommand, "55 89 E5 57 56 53 83 EC 1C 8B 75 ? 8B 5D ? 81 FE F
8787
// EngineDemoPlayer
8888
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")
8989

90+
// CModelLoader
91+
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")
92+
9093
// MaterialSystem
9194
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")
9295

src/Offsets/Portal 2 9568.hpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,20 @@ SIGSCAN_DEFAULT(InterpolateDemoCommand, "55 8B EC 83 EC 10 56 8B F1 8B 4D 10 57
484484
"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")
485485

486486

487+
// CModelLoader
488+
// win: "modelprecache" xref -> client string-table update callback -> CModelLoader vtable +0x1C call with flag 4; global immediate is g_pModelLoader
489+
// linux: "CClientState::ConsistencyCheck" xref -> model consistency type 3 block -> CModelLoader vtable +0x1C call with flag 4; global immediate is g_pModelLoader
490+
SIGSCAN_DEFAULT(CModelLoaderModelPrecache,
491+
"8B 0D ? ? ? ? 8B 11 6A 04 50 8B 42 1C FF D0 50 EB 02 6A 00",
492+
"A1 ? ? ? ? 83 EC 04 8B 10 6A 04 FF B5 ? ? ? ? 50 FF 52 1C 89 85 ? ? ? ? 83 C4 10 85 C0")
493+
OFFSET_DEFAULT(CModelLoaderModelPrecacheGlobal, 2, 1)
494+
OFFSET_DEFAULT(CModelLoaderEntryArray, 0x8, 0x8) // "CModelLoader::FindModel: NULL name" xref -> successful lookup path reads [this+8] + index*0x10 + 0xC
495+
OFFSET_DEFAULT(CModelLoaderEntryCount, 0x16, 0x16) // same CModelLoader::FindModel tree/list state; active count is this+0x16
496+
OFFSET_DEFAULT(CModelLoaderEntryStride, 0x10, 0x10) // same CModelLoader::FindModel lookup path; entry nodes are 0x10 bytes
497+
OFFSET_DEFAULT(CModelLoaderEntryModel, 0xC, 0xC) // same CModelLoader::FindModel lookup path; entry+0xC is model_t *
498+
OFFSET_DEFAULT(CModelLoaderModelFlags, 0x108, 0x108) // CModelLoader vtable +0x1C target ORs caller flags into model_t+0x108
499+
500+
487501
// Matchmaking
488502
SIGSCAN_DEFAULT(UpdateLeaderboardData, "55 8B EC 83 EC 08 53 8B D9 8B 03 8B 50 08",
489503
"55 89 E5 57 56 53 83 EC 2C 8B 45 08 8B 5D 0C")

0 commit comments

Comments
 (0)