diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-06 16:09:55 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-06 16:09:55 +0100 |
| commit | 33f5ed9ef58f26c6b3b52cf605dd4311d658a60d (patch) | |
| tree | a97254872e465c9107b7ef19695ab56bb076d7e2 /FIX_PLAN.md | |
| parent | Items: log the usable ammo table once (diff) | |
| download | hl2h-33f5ed9ef58f26c6b3b52cf605dd4311d658a60d.tar.gz hl2h-33f5ed9ef58f26c6b3b52cf605dd4311d658a60d.zip | |
Fix audit findings: crash safety, correctness, rendering, performance
Crash/UB:
- gate every hook behind an init-stage flag until config/surface/input are ready
- publish module maps as immutable atomic snapshots instead of reallocating
shared vectors under unlocked readers
- validate the %ebp walk in the RandomSeed/RandomFloat hooks against the stack
- bounds-check every RecvTable/RecvProp/string dereference with module_range()
- preserve the Itanium vtable header when cloning (client, CInput, model render,
studio render)
- reject negative usercmd sequence numbers; guard config_load against a null
section target; cap the bone parent walk; check mprotect; drop PROT_EXEC on
the GOT patch; absorb server.so .bss tail
Correctness:
- filter dormant entities across ESP, aimbot, triggerbot, melee, items and
projectiles (slot self-tested against the local player)
- reset the server-health latch per map and only latch on valid model indices
- bone cache sized for the 128-target scan; set valid=false on every failure;
head_position keeps searching past an invalid bone
- vischeck steps past glass by probing until clear, and ignores entities owned
by the target (held weapons, bone followers)
- aimbot: maxs no longer disables it, AABB-aware FOV prefilter, lock hysteresis,
lead point re-checked for FOV and visibility
- spread: seed hook arms on the no-spread toggle alone; engine toggle only
controls the RandomFloat zeroing
- resolve m_hOwnerEntity/m_hOwner; NPC lead velocity from the server entity
- vector_angles pitch range, point_ground normal test, underwater jump+duck no
longer skips combat, semi-auto press/release edge for synthetic fire
- item/projectile markers and blast rings refresh origin per frame while
discovery stays at 10 Hz; trajectory uses the real m_DmgRadius and honours
the blast-radius toggle
Rendering/GUI:
- one overlay draw per engine frame (FRAME_RENDER_START boundary), fixing
alpha stacking and the speed-graph flicker
- clipped lines/outlines and ellipsised text instead of drawing past the
content pane or shifting over neighbours
- dropdown dismissal consumes the click; hover ignores hidden controls
- chams: reset AlphaModulate, reference shared materials once, restore prop
tint colour; fonts created once per handle
- projection uses Source's 4:3 aspect scaling and roll; screen size refreshed
each frame; FOV circle gated on the aim toggles, not the bind
Performance:
- TTL caches for NPC relation, friendly/neutral and damage class
- model names memoised per model index
- entity pointer -> index cache for the triggerbot (was a 4096-slot walk/tick)
Build:
- real Makefile with -MMD dependency tracking, debug/release objects and an
always-relink rule; build.sh kept as a thin wrapper
Diffstat (limited to 'FIX_PLAN.md')
| -rw-r--r-- | FIX_PLAN.md | 802 |
1 files changed, 802 insertions, 0 deletions
diff --git a/FIX_PLAN.md b/FIX_PLAN.md new file mode 100644 index 0000000..e4e149e --- /dev/null +++ b/FIX_PLAN.md | |||
| @@ -0,0 +1,802 @@ | |||
| 1 | # hl2h fix plan | ||
| 2 | |||
| 3 | Derived from a full audit of `src/` (12,243 lines) plus `build.sh` / `play.sh`. | ||
| 4 | Every item below was re-verified against the working tree; claims that did not | ||
| 5 | survive verification are listed in [§6](#6-verified-not-a-bug--do-not-fix) so | ||
| 6 | nobody wastes time on them. | ||
| 7 | |||
| 8 | Ordering: **P0 → P1 → P2 → P3**. Each phase leaves the cheat playable. | ||
| 9 | Work inside a phase is independent enough to parallelise; cross-phase | ||
| 10 | dependencies are noted. | ||
| 11 | |||
| 12 | --- | ||
| 13 | |||
| 14 | ## 0. Verification ground rules | ||
| 15 | |||
| 16 | - Line numbers are from the audited tree; re-check after edits. | ||
| 17 | - "Fix" means *smallest change that removes the failure mode*, not a rewrite. | ||
| 18 | Rewrites belong to P3. | ||
| 19 | - Every P0/P1 item must be reproducible before the fix and re-checked after. | ||
| 20 | For logic fixes, extract the function into `tests/` (see §5) rather than | ||
| 21 | relying on eyeballing a running game. | ||
| 22 | - Anything touching hook install/uninstall goes in with a readiness guard | ||
| 23 | (P0-1), otherwise later phases regress the race. | ||
| 24 | |||
| 25 | --- | ||
| 26 | |||
| 27 | ## 1. P0 — crash, UB, and startup races | ||
| 28 | |||
| 29 | These are the items that produce the existing `crash` log and the highest risk | ||
| 30 | of nondeterministic hangs. Do them first, in this order. | ||
| 31 | |||
| 32 | ### P0-1 Hook readiness gate and init ordering *(M)* | ||
| 33 | **Where:** `hl2h.cpp:1185` (`chams_init`), `:1204-1206` (`init_conmsg`, | ||
| 34 | `install_hook`, `install_menu`), `menu.cpp:1224` (`config_load`), | ||
| 35 | `menu.cpp:1229` (ISurface discovery). | ||
| 36 | |||
| 37 | **Problem:** `CHLClient` and `CInput` hooks are installed *before* config load | ||
| 38 | and ISurface discovery. The game thread can enter `hkdCreateMove` / | ||
| 39 | `hkdInputCreateMove` / `hkdModelExecute` while `setup_thread` is still writing | ||
| 40 | feature globals and `g_surface`. Every hook is also installed lazily from | ||
| 41 | different threads (surface hooks from `CreateMove`, studio-prop hooks from the | ||
| 42 | paint path), so "is init done" is not observable. | ||
| 43 | |||
| 44 | **Fix:** | ||
| 45 | 1. Add `std::atomic<int> g_init_stage {0}` (`0` = nothing, `1` = interfaces + | ||
| 46 | netvars, `2` = config + surface + fonts, `3` = hooks live). | ||
| 47 | 2. Move `install_hook()`, `install_input_hook()`, and the seed/uniform patches | ||
| 48 | to *after* `install_menu()` completes. | ||
| 49 | 3. Every hook entry point starts with | ||
| 50 | `if (g_init_stage.load(std::memory_order_acquire) < 3) return call_original(...);` | ||
| 51 | — hooks stay installed, they just pass through until ready. | ||
| 52 | 4. Keep lazy surface/studio installs, but have them check the stage instead of | ||
| 53 | relying on "first `CreateMove` implies ready". | ||
| 54 | 5. On any setup failure, set stage to a terminal `-1` and log once; hooks then | ||
| 55 | pass through forever instead of half-working. | ||
| 56 | |||
| 57 | **Verify:** start the game straight into a map (`+map`) and into the main menu, | ||
| 58 | 10× each, with `HL2H_DEBUG=1`; `hook installed:` lines must now appear after | ||
| 59 | `config loaded:` / `surface resolved:`. No crash in `crash`-style backtraces. | ||
| 60 | |||
| 61 | --- | ||
| 62 | |||
| 63 | ### P0-2 Module snapshots are published under a lock but read unlocked *(M)* | ||
| 64 | **Where:** writer `modules.cpp:78-84`; readers `hl2h.h:47-52` | ||
| 65 | (`Module::contains` / `valid` / `.ranges` / `.path`), `netvars.cpp:9`, | ||
| 66 | `server.cpp:71,95,147`, `hl2h.cpp:1104-1109`. Re-entry: `server.cpp:147-148` | ||
| 67 | calls `find_modules()` from the game thread while `setup_thread` reads | ||
| 68 | `g_client`/`g_engine`. | ||
| 69 | |||
| 70 | **Problem:** `find_modules()` move-assigns `g_client`/`g_engine`/`g_server`, | ||
| 71 | freeing `std::vector<Range>` and `std::string` buffers while another thread | ||
| 72 | iterates them. Locking only the writer is worse than no lock — it hides the | ||
| 73 | race. | ||
| 74 | |||
| 75 | **Fix:** | ||
| 76 | 1. Make `Module` immutable after publish. Keep a | ||
| 77 | `std::shared_ptr<const ModuleSet>` and publish with | ||
| 78 | `std::atomic_store(&g_modules, new_set)` (C++11 `shared_ptr` atomic free | ||
| 79 | functions; C++17 build is fine). | ||
| 80 | 2. Readers do `auto m = std::atomic_load(&g_modules);` once per function and | ||
| 81 | use `m->client` for the whole body. No lock, no torn state. | ||
| 82 | 3. Stop calling `find_modules()` from `server.cpp` / `netvars.cpp` on demand. | ||
| 83 | Resolve once during init; on failure, retry from the setup thread only. | ||
| 84 | If a late retry is genuinely needed, it must publish a new snapshot, never | ||
| 85 | mutate the current one. | ||
| 86 | |||
| 87 | **Verify:** run `resolve_server_netvars` with a deliberate 100 ms delay under | ||
| 88 | `-fsanitize=thread` on a native (non-`-m32`) build of `modules.cpp` only if | ||
| 89 | practical; otherwise add an assertion that `find_modules` is only called from | ||
| 90 | the setup thread. | ||
| 91 | |||
| 92 | --- | ||
| 93 | |||
| 94 | ### P0-3 `%ebp` frame-pointer dereference in the random hooks *(M)* | ||
| 95 | **Where:** `hl2h.cpp:497-501` (`hkdSrvRandomSeed`) and `:606-610` | ||
| 96 | (`hkdUniformRandomFloat`). `build.sh` has `-fno-omit-frame-pointer`, but that | ||
| 97 | only constrains *this* `.so`; the callers are Valve's `server.so` and | ||
| 98 | `libvstdlib.so`. | ||
| 99 | |||
| 100 | **Problem:** `frame = *(void **)ebp; self = *(void **)(frame + 8)` assumes a | ||
| 101 | standard frame chain and a first-argument-at-`+8` layout in code we do not | ||
| 102 | control. A non-frame-pointer caller, tail call, or different convention | ||
| 103 | segfaults immediately. | ||
| 104 | |||
| 105 | **Fix (in order of preference):** | ||
| 106 | 1. Drop the `self` derivation entirely. `g_spread_rand_left` only needs to be | ||
| 107 | armed while the *local player* fires; `hkdSrvRandomSeed` already knows the | ||
| 108 | seed window is being requested from the local server entity path | ||
| 109 | (`g_local_server_player` is available via `server_active_weapon`-style | ||
| 110 | lookups). Arm the window on `CreateMove` when `IN_ATTACK` is set and the | ||
| 111 | active weapon has spread, then let the hook only substitute the seed. | ||
| 112 | 2. If the frame walk stays, hard-validate before both dereferences: | ||
| 113 | `contains_range(ebp, 4) && (ebp & 3) == 0`, same for `frame` and | ||
| 114 | `frame + 8`, and require the resulting `self` to be inside a mapped module | ||
| 115 | or the entity-list heap range. Bail to the original on any failure. | ||
| 116 | 3. Never leave `g_spread_rand_left` armed across ticks: reset it in | ||
| 117 | `do_move` entry and on `CreateMove` return. | ||
| 118 | |||
| 119 | **Verify:** pistols/SMG/AR2/shotgun/crossbow, 20 shots each, log | ||
| 120 | `seed in / seed out / fallback` counters; zero fallbacks on a normal map, no | ||
| 121 | crash on maps with scripted weapons. | ||
| 122 | |||
| 123 | --- | ||
| 124 | |||
| 125 | ### P0-4 Netvar scan bounds validation *(S)* | ||
| 126 | **Where:** `netvars.cpp:9-19` (`module_string` checks only the first byte), | ||
| 127 | `:33-39` (derefs `table+0`, `table+4`, `table+0xC` before validating `table`), | ||
| 128 | `:317` (root pointer from `cc+0xC` unvalidated), plus the nested `ptable` path. | ||
| 129 | |||
| 130 | **Fix:** add one helper next to `Module`: | ||
| 131 | ```cpp | ||
| 132 | bool module_range(uintptr_t p, size_t n); // p and p+n-1 inside one range | ||
| 133 | ``` | ||
| 134 | Use it for every pointer before the first dereference and for every string | ||
| 135 | scan (`module_range(p, outn)`), then keep the existing printable-char check. | ||
| 136 | Add an `assert(g_client.contains(table))`-style precondition in | ||
| 137 | `collect_netvars` so future callers fail loudly instead of reading garbage. | ||
| 138 | |||
| 139 | **Verify:** `resolve_netvars` logs "resolved N/M" on main menu, map load, and | ||
| 140 | map change; N/M must not change. No crash when `server.so` is absent. | ||
| 141 | |||
| 142 | --- | ||
| 143 | |||
| 144 | ### P0-5 Preserve the Itanium vtable header when hooking *(S)* | ||
| 145 | **Where:** `hl2h.cpp:1060-1068`, `cinput.cpp:153-162`, | ||
| 146 | `studio.cpp:544-548`, `studio.cpp:584-598`. | ||
| 147 | |||
| 148 | **Problem:** all four sites do `new void*[N]; memcpy(new, vt, N*4); ptr = new;` | ||
| 149 | so `new[-1]` (typeinfo) and `new[-2]` (offset-to-top) are heap metadata. | ||
| 150 | The cheat never calls RTTI on these pointers, so this is currently latent, but | ||
| 151 | the engine may, and the fix costs nothing. | ||
| 152 | |||
| 153 | **Fix:** allocate `N+2`, set `dst[0]=vt[-2]; dst[1]=vt[-1];` and memcpy the | ||
| 154 | slots at `dst+2`; hand out `dst+2`. Add a tiny helper | ||
| 155 | `void** clone_vtable(void** vt, size_t slots)` used by all four sites — this | ||
| 156 | also removes the duplicated 256/32/49 magic sizes. | ||
| 157 | |||
| 158 | **Verify:** hooks still fire (`logd` in each handler); no behaviour change | ||
| 159 | expected. Optional: `nm`/`objdump` sanity that the new header words match. | ||
| 160 | |||
| 161 | --- | ||
| 162 | |||
| 163 | ### P0-6 `config_load` null deref on malformed config *(S)* | ||
| 164 | **Where:** `menu.cpp:347-385` sets `cur = nullptr` for `[view]`, | ||
| 165 | `[movement]`, `[chams_*]`; `menu.cpp:643-646` dereferences it unconditionally. | ||
| 166 | |||
| 167 | **Reachability:** `config_save` never writes `active`/`bind` there, so this is | ||
| 168 | hand-edited/corrupt-file only. Still a one-line crash. | ||
| 169 | |||
| 170 | **Fix:** the section loop should carry an explicit "target kind" enum | ||
| 171 | (`TARGET_FEATURE`, `TARGET_SCALARS`, `TARGET_CHAMS`, `TARGET_IGNORE`) and the | ||
| 172 | key handler should switch on it, not on `cur`. Minimum change: | ||
| 173 | `if (!cur) continue;` before the `active`/`bind` branches. | ||
| 174 | |||
| 175 | **Verify:** add the malformed cases to `tests/` (§5) — `[view]\nactive=1`, | ||
| 176 | `[chams_vm]\nbind=3`, truncated last line, no trailing newline. | ||
| 177 | |||
| 178 | --- | ||
| 179 | |||
| 180 | ### P0-7 Signed modulo on `seq` in `cinput.cpp` *(S)* | ||
| 181 | **Where:** `cinput.cpp:61`, `:72-73`, `:141`. | ||
| 182 | |||
| 183 | **Problem:** `seq % 90` is negative for negative `seq`; `(size_t)` then makes a | ||
| 184 | huge index. The `command_number` sanity check happens *after* the pointer is | ||
| 185 | formed and dereferenced. | ||
| 186 | |||
| 187 | **Fix:** `if (seq < 0) return;` at the top of `usercmd_get` / | ||
| 188 | `fix_verified_crc` / `hkdInputCreateMove`, and use | ||
| 189 | `((unsigned)seq) % 90u` throughout. | ||
| 190 | |||
| 191 | **Verify:** assertion `seq >= 0`; no behaviour change in normal play. | ||
| 192 | |||
| 193 | --- | ||
| 194 | |||
| 195 | ### P0-8 Defensive guards with real (if unlikely) triggers *(S, batch)* | ||
| 196 | Small, independent, cheap: | ||
| 197 | |||
| 198 | | Item | Where | Fix | | ||
| 199 | |---|---|---| | ||
| 200 | | Unbounded parent walk | `bones.cpp:419-423` | 64-iteration cap, same as the humanoid branch | | ||
| 201 | | `mprotect` unchecked, `PROT_EXEC` on data, unaligned slot | `hl2h.cpp:571-573`, `:646-651` | check return; touch the full page range spanning the slot; `PROT_READ\|PROT_WRITE` only; restore original prot after the write | | ||
| 202 | | `server.so` `.bss` tail not absorbed | `modules.cpp:50-58` | add the `server` branch alongside client/engine | | ||
| 203 | | studio `name[64]` read as 128 | `studio.cpp:64-70` | `strnlen(name, 64)`; note this reads adjacent header fields, not OOB memory, so it is a correctness fix only | | ||
| 204 | | `bone_nearest_core_parent` / `build_core_mask` recomputed every tick | `bones.cpp:346-362` | recompute only when `hdr`/`count` changes | | ||
| 205 | |||
| 206 | **Verify:** map with many NPCs (e.g. `d2_coast_03`), skeleton ESP on, 10 min; | ||
| 207 | `bones_update` budget logged once per second must stay flat. | ||
| 208 | |||
| 209 | --- | ||
| 210 | |||
| 211 | ## 2. P1 — gameplay and data-correctness bugs | ||
| 212 | |||
| 213 | ### P1-1 Dormant-entity handling *(M)* | ||
| 214 | **Where:** none of `esp_collect` (`esp.cpp:436`), `select_aim_target` (`:916`), | ||
| 215 | `melee_aimbot_apply` (`:1375`), `triggerbot_apply` (`:1732`), | ||
| 216 | `items_collect` (`:2740`, `:2801`), `projs_collect` (`:3090`), | ||
| 217 | `prop_tint_update` (`:1797`) checks `IsDormant()`. | ||
| 218 | |||
| 219 | **Fix:** one helper `bool entity_dormant(void* ent)` in the entity-query layer | ||
| 220 | (P3 target: `entity_query.cpp`), plus a call at the top of each loop. For | ||
| 221 | aimbot/triggerbot/melee this is a correctness fix (stale frozen positions); | ||
| 222 | for items/projectiles it is a ghost-marker fix. Do **not** add it to | ||
| 223 | `bones_update` alone — gate at collection so downstream code never sees them | ||
| 224 | (also removes the "SetupBones on a dormant entity" class of crash). | ||
| 225 | |||
| 226 | **Verify:** walk out of an NPC's PVS; box/arrow/skeleton disappears within one | ||
| 227 | tick, aimbot stops acquiring it, and no `schema`/studio asserts appear in | ||
| 228 | console. | ||
| 229 | |||
| 230 | ### P1-2 `srv_checked` permanent latch *(S)* | ||
| 231 | **Where:** `esp.cpp:433-434`, `:459-464`. Reset block exists at | ||
| 232 | `hl2h.cpp:784-793` but does not touch it. | ||
| 233 | |||
| 234 | **Fix:** move the two statics into the map-change reset registry (P3-4; a | ||
| 235 | temporary `esp_reset_session()` called from `hl2h.cpp:784` is enough now), and | ||
| 236 | only latch when `cli_mi > 0 && srv_mi > 0`. Keep re-checking per NPC for the | ||
| 237 | first ~30 s after a map load rather than once ever. | ||
| 238 | |||
| 239 | **Verify:** load map → immediately look at an NPC before it finishes spawning | ||
| 240 | → health bar must appear once the model index syncs. | ||
| 241 | |||
| 242 | ### P1-3 Bone cache correctness *(M)* | ||
| 243 | **Where:** `bones.cpp:103` (`g_entries[64]`), `:121-125` (`g_next++ % 64`), | ||
| 244 | `:322-330` (early returns leave `valid == true`), `:494-497` | ||
| 245 | (`head_position` returns on the first name match even if invalid). | ||
| 246 | |||
| 247 | **Fix:** | ||
| 248 | 1. Size the cache for the real upper bound (128 targets) with headroom: 256 | ||
| 249 | entries, LRU by stamp, or a `std::unordered_map<int, BoneEntry>` keyed by | ||
| 250 | entity index. Never evict an entry whose `stamp` is the current tick. | ||
| 251 | 2. Set `e->valid = false` on *every* failure path, including `:322-330`. | ||
| 252 | 3. `head_position`: `continue` instead of `return` when `hitbox_world` / | ||
| 253 | `bone_pos` fails, so the fallback chain still runs. | ||
| 254 | 4. Make `bones_update` return the cached entry pointer so callers cannot read | ||
| 255 | a different (evicted) entry than the one that was validated. | ||
| 256 | |||
| 257 | **Verify:** map with >64 NPCs (or a `npc_create` stress test); skeleton ESP | ||
| 258 | must not flicker; `bones_count` must never return a count for an entity whose | ||
| 259 | `SetupBones` failed this tick. | ||
| 260 | |||
| 261 | ### P1-4 Vischeck: glass stepping and the entity filter *(M)* | ||
| 262 | **Where:** `vischeck.cpp:179-214` (step 0.5 past `tr.endpos`), `:189-190` | ||
| 263 | (`startsolid` → `false`), `:91-99` (`ShouldHitEntity` ignores only `self` and | ||
| 264 | `target`). | ||
| 265 | |||
| 266 | **Fix:** | ||
| 267 | 1. Replace the fixed 0.5 step with "advance until outside the brush, up to | ||
| 268 | N units, then continue", i.e. loop the trace forward while `startsolid` or | ||
| 269 | the hit entity is still the same glass brush, capped at, say, 32 units and | ||
| 270 | never past `point`. | ||
| 271 | 2. `ShouldHitEntity` must also ignore the target's children: resolve | ||
| 272 | `m_hOwnerEntity` (needs P1-7) and skip anything whose owner is `target`; | ||
| 273 | that covers held weapons and `phys_bone_follower`. | ||
| 274 | 3. Early-out when `start` is within epsilon of `point` instead of tracing | ||
| 275 | backwards. | ||
| 276 | |||
| 277 | **Verify:** shoot through `func_breakable` glass thick and thin, strider and | ||
| 278 | gunship hitboxes, and a target holding a weapon — ESP visibility and aimbot | ||
| 279 | vischeck must agree with manual LOS. | ||
| 280 | |||
| 281 | ### P1-5 Aimbot selection and firing *(M)* | ||
| 282 | **Where:** `esp.cpp:815-816` (hard `maxs < 0`), `:943-945` (fallback exists), | ||
| 283 | `:949-953` (single `approx` prefilter), `:887` (lock with `enforce_fov=false`), | ||
| 284 | `:1079-1084` (lead applied after vischeck/FOV), `:1199`, `:1532`, `:1751` | ||
| 285 | (unconditional `IN_ATTACK`). | ||
| 286 | |||
| 287 | **Fix:** | ||
| 288 | 1. Delete `g_net.maxs < 0` from the gate at `:815`; rely on the | ||
| 289 | `maxz = ... : 72.0f` fallback. | ||
| 290 | 2. Prefilter: use a capsule/segment from the entity's `origin.z` to | ||
| 291 | `origin.z + maxz` and test distance to the view ray, or simply widen | ||
| 292 | `max_fov` by the angular radius of the AABB (`atan(maxRadius/dist)`) before | ||
| 293 | rejecting. Then keep the exact per-hitbox test in `try_target`. | ||
| 294 | 3. Lock: keep the lock but re-apply the FOV test with hysteresis (e.g. allow | ||
| 295 | up to `max_fov * 1.5` before dropping), and key the lock on | ||
| 296 | `(entindex, serial)` from `CBaseHandle`/`entindex` so slot reuse cannot | ||
| 297 | transfer it. | ||
| 298 | 4. Lead: compute `lead` first, then run vischeck and FOV on `lead`. If the | ||
| 299 | lead point fails, fall back to the un-led point rather than firing at a | ||
| 300 | wall. | ||
| 301 | 5. Fire control: track the tick of the last synthetic `IN_ATTACK` and clear | ||
| 302 | the button for one tick after each shot (a small state machine per | ||
| 303 | weapon), or drive `IN_ATTACK` from `m_afButtonPressed` semantics so | ||
| 304 | semi-autos see a release edge. | ||
| 305 | |||
| 306 | **Verify:** per weapon class (pistol, SMG, AR2, shotgun, crossbow, RPG) run | ||
| 307 | 10-shot strings at short/medium/long range, log chosen target id, hitbox, and | ||
| 308 | whether FOV/vischeck passed on the *led* point. | ||
| 309 | |||
| 310 | ### P1-6 Spread control gate *(S)* | ||
| 311 | **Where:** `hl2h.cpp:505-507` and `:601-602` require | ||
| 312 | `spread_no_spread_enabled() && spread_engine_enabled()`; `esp.cpp:1659` only | ||
| 313 | requires `no_spread`. | ||
| 314 | |||
| 315 | **Problem:** turning off "Engine spread override" disables the seed hook | ||
| 316 | entirely, so "seed selection only" does nothing (documented at | ||
| 317 | `hl2h.cpp:474`). | ||
| 318 | |||
| 319 | **Fix:** decouple the two gates: the seed-substitution hook is armed by | ||
| 320 | `spread_no_spread_enabled()`; `spread_engine_enabled()` only controls the | ||
| 321 | `RandomFloat`→0 override. Update the GUI tooltip text if the semantics change. | ||
| 322 | |||
| 323 | **Verify:** with engine override off, 20 shotgun blasts: pellet grouping must | ||
| 324 | be tight and repeatable. | ||
| 325 | |||
| 326 | ### P1-7 Resolve `m_hOwnerEntity` / `m_hOwner` *(S)* | ||
| 327 | **Where:** `netvars.cpp` has no branch for either; `g_net.owner` / | ||
| 328 | `g_net.weapon_owner` stay `-1` (`hl2h.h:99-100`) yet are read at | ||
| 329 | `esp.cpp:2253-2259`, `:2272-2275`. | ||
| 330 | |||
| 331 | **Fix:** add both name matches in the netvar walk (with the P0-4 range checks), | ||
| 332 | then use them for (a) the vischeck filter in P1-4, (b) weapon ownership in | ||
| 333 | `items_collect`. | ||
| 334 | |||
| 335 | **Verify:** item ESP must stop marking freshly dropped weapons as owned; | ||
| 336 | vischeck must stop being blocked by held weapons. | ||
| 337 | |||
| 338 | ### P1-8 Math and movement correctness *(S)* | ||
| 339 | | Item | Where | Fix | | ||
| 340 | |---|---|---| | ||
| 341 | | `vector_angles` pitch wraps to `[0,360)` | `hl2h.cpp:90-94` | return `[-90,90]`; normalise yaw to `[-180,180]`; delete the `.x = 0` workaround at `:865` | | ||
| 342 | | Early `return` skips all features underwater | `hl2h.cpp:855-859` | set the underwater flag and `goto`/`break` past only the bhop section; never skip `note_modified()` | | ||
| 343 | | `point_ground` ignores surface normal | `vischeck.cpp:273` | require `tr.normal.z >= 0.7f` like `ground_distance` (`:291`) | | ||
| 344 | | `jb_force_next_distance` casts away `const`, cached `vz` goes stale | `hl2h.cpp:286-304`, `:334`, `:459` | drop the cached `vz`, re-read `vel[2]` after the call, or return the new value | | ||
| 345 | | melee AABB has no lower bounds | `esp.cpp:1420-1431` | reject `minx/miny/minz` outside `±512` | | ||
| 346 | | triggerbot O(4096) index scan | `esp.cpp:1729-1758` | use `IClientNetworkable::entindex()` on the hit entity | | ||
| 347 | |||
| 348 | **Verify:** underwater jump+duck still bhops and still shoots; edge-jump at a | ||
| 349 | steep-slope ledge; aimbot aims correctly when looking up (pitch sign). | ||
| 350 | |||
| 351 | ### P1-9 Cache/table lifetime bugs *(S)* | ||
| 352 | **Where:** `esp.cpp:3060-3073` (`proj_first_seen` 32 fixed slots, no | ||
| 353 | eviction); `esp.cpp:2719-2774` (`owned_mask` OR'd within a map). | ||
| 354 | |||
| 355 | **Fix:** replace `proj_first_seen` with a 64-entry ring keyed by | ||
| 356 | `(entindex, serial)` that overwrites the oldest entry; recompute `owned_mask` | ||
| 357 | from `server_player_weapon(local_idx, slot)` on every scan when the server | ||
| 358 | interface is available (keep the OR as fallback only). | ||
| 359 | |||
| 360 | **Verify:** throw >40 grenades on one map; fuse timers must stay correct. Drop | ||
| 361 | and re-pick a weapon; item ESP must reclassify it. | ||
| 362 | |||
| 363 | ### P1-10 Projectile lead source *(S)* | ||
| 364 | **Where:** `esp.cpp:1045-1048` reads `C_BaseEntity::m_vecVelocity` on NPC | ||
| 365 | targets, but Source only networks that field for the player | ||
| 366 | (`baseentity.cpp:6665`), so NPC lead velocity is stale/zero. | ||
| 367 | `g_srv_velocity` is resolved at `server.cpp:161` and never used. | ||
| 368 | |||
| 369 | **Fix:** for NPC targets, read velocity through the server entity path | ||
| 370 | (`g_srv_velocity`) and fall back to zero lead on failure. Grenades | ||
| 371 | (`esp.cpp:3185-3187`) are fine — they do network velocity. | ||
| 372 | |||
| 373 | **Verify:** crossbow/RPG lead on a moving combine at range; the led point | ||
| 374 | should lead the NPC's actual movement. | ||
| 375 | |||
| 376 | ### P1-11 Visual toggles gated by the wrong feature *(S)* | ||
| 377 | - `draw_fov_circle` is inside `esp_draw` (`esp.cpp:2084`, gated by | ||
| 378 | `FEAT_ESP` at `menu.cpp:1083`) so it shows with aimbot off and hides with | ||
| 379 | ESP off. Move the call to a neutral draw path or gate it on | ||
| 380 | `aimbot_active || silentaim_active`. | ||
| 381 | - `draw_transitions` (`esp.cpp:2092`) has no setting at all. Add a toggle and | ||
| 382 | default it off (its `transitions_scan` walks server entities every frame). | ||
| 383 | - `draw_fov_circle` rejects `fov >= 89` (`esp.cpp:606`) while the slider | ||
| 384 | allows 180 (`gui.cpp:371`). Clamp the slider to the drawable range, or | ||
| 385 | render the cone as a screen-space wedge instead of projecting axis points. | ||
| 386 | |||
| 387 | **Verify:** toggle ESP off with aimbot on → circle stays; toggle aimbot off | ||
| 388 | with ESP on → circle gone; transitions toggle actually stops the wireframes. | ||
| 389 | |||
| 390 | --- | ||
| 391 | |||
| 392 | ## 3. P2 — rendering and GUI correctness | ||
| 393 | |||
| 394 | ### P2-1 Draw exactly once per frame *(M)* | ||
| 395 | **Where:** `hl2h.cpp:1008-1032`; guard is only `g_in_draw` (`:1003`), which | ||
| 396 | prevents nesting, not repetition. Both `hkdDrawPrintText` (`:1017`) and | ||
| 397 | `hkdPaintTraverseEx` (`:1027-1031`) call `outer_draw()`. | ||
| 398 | |||
| 399 | **Fix:** pick one owner. Preferred: `PaintTraverseEx` filtered to | ||
| 400 | `MatSystemTopPanel` (or the engine's root panel), and use `DrawPrintText` | ||
| 401 | only as a fallback if no paint happened in the current frame | ||
| 402 | (`last_paint_frame != g_frame_count`). Add a per-frame id so the guard is | ||
| 403 | "already drew this frame", not "currently drawing". | ||
| 404 | |||
| 405 | **Secondary (independent):** save and restore ISurface text state around | ||
| 406 | `outer_draw()` — `m_hCurrentFont` (`this+0x290`), `drawColor` (`+0x24`), | ||
| 407 | `textColor` (`+0x28`), `m_nTextX/Y` (`+0x2c/0x30`) — because | ||
| 408 | `PushMakeCurrent`/`PopMakeCurrent` only unwinds the clip stack | ||
| 409 | (`ISURFACE_STATUS.md:64-67`). | ||
| 410 | |||
| 411 | **Verify:** alpha-255 world tint and a 120-alpha ESP box must look identical | ||
| 412 | whether ESP is drawn once or with the old code; instrument a frame counter and | ||
| 413 | assert exactly one `outer_draw` per frame. | ||
| 414 | |||
| 415 | ### P2-2 Single source of truth for widget geometry *(S now, folds into P3-2)* | ||
| 416 | **Where:** `gui.cpp:1347-1351` (`cr.x` includes `+S(10)`), | ||
| 417 | `:1948` and `:1627` (track from `cr.x`), `:1854-1855` (drag range omits the | ||
| 418 | `+S(10)` and is 10 px wider at the right edge). | ||
| 419 | |||
| 420 | **Immediate fix:** compute the track rect once in a helper | ||
| 421 | `slider_track(const Rect& cr)` and call it from `draw_slider`, | ||
| 422 | `handle_mouse_click`, and the drag handler. Add the missing `+S(10)` in the | ||
| 423 | drag range as a one-line stopgap if the helper is deferred. | ||
| 424 | |||
| 425 | **Verify:** click a slider knob at the far left/right, drag across; value must | ||
| 426 | track the cursor 1:1 and not jump on mouse-down. | ||
| 427 | |||
| 428 | ### P2-3 Clipping correctness *(S)* | ||
| 429 | | Bug | Where | Fix | | ||
| 430 | |---|---|---| | ||
| 431 | | `line()` has no clip | `gui.cpp:920` | add `const Rect* clip` and clip via the same Cohen–Sutherland helper used for rects; use it for the checkbox checkmark (`:1941-1942`) and picker cursors (`:1300-1301`, `:1313-1314`) | | ||
| 432 | | `outline()` draws a border on the clip edge | `gui.cpp:911-918` | clip each of the four edges independently instead of outlining the clamped rect | | ||
| 433 | | `text_in()` shifts text left instead of clipping | `gui.cpp:872-888` | honour `max_w`: binary-search the longest prefix that fits, append `…`; never move `x` left | | ||
| 434 | |||
| 435 | **Verify:** scroll a card halfway past the content top/bottom — no border line | ||
| 436 | through the card, no checkmark floating in the header, long dropdown labels | ||
| 437 | truncate instead of overlapping the label column. | ||
| 438 | |||
| 439 | ### P2-4 Pointer/click discipline in the GUI *(S)* | ||
| 440 | **Where:** `gui.cpp:1501-1522` (dropdown dismissal falls through), | ||
| 441 | `:1717-1772` (`handle_hover` ignores `g_dropdown_ctrl`). | ||
| 442 | |||
| 443 | **Fix:** a single `consume_click()` result from the dropdown block; when it | ||
| 444 | closes, return immediately. `handle_hover` early-outs while a dropdown, colour | ||
| 445 | picker drag, or slider drag is active. Also close pickers/dropdowns on | ||
| 446 | outside-click consistently (currently pickers need another colour control). | ||
| 447 | |||
| 448 | **Verify:** open a dropdown over a toggle row and click the toggle; the | ||
| 449 | dropdown must close and the toggle must not flip. | ||
| 450 | |||
| 451 | ### P2-5 SDL interposition state tracking *(M)* | ||
| 452 | **Where:** `gui.cpp:140-141` (`dlsym(RTLD_DEFAULT, ...)` resolves the cheat's | ||
| 453 | own exported wrapper), `:2415-2434` (requests dropped while open), | ||
| 454 | `:1382-1385` (restores the pre-open snapshot), `:1410-1412` (snapshot). | ||
| 455 | |||
| 456 | **Fix:** | ||
| 457 | 1. Resolve real symbols with `dlsym(RTLD_NEXT, ...)` in both wrappers (they | ||
| 458 | already do internally at `:2417`/`:2430`; the public `sd_*` pointers must | ||
| 459 | match). | ||
| 460 | 2. Track *last requested* state while open: in the wrappers, record | ||
| 461 | `g_game_wants_cursor` / `g_game_wants_relative` before forcing the GUI | ||
| 462 | state, and restore those on close instead of the pre-open snapshot. | ||
| 463 | 3. Don't drop all `SDL_MOUSEMOTION`; drop only the relative-motion deltas the | ||
| 464 | game would consume, or key off `g_open` + "not over a GUI window". | ||
| 465 | 4. Keep the `MOUSEWHEEL` watch but honour `direction == FLIPPED` | ||
| 466 | (`SDL_MouseWheelEvent` at `+24`). | ||
| 467 | |||
| 468 | **Verify:** open GUI on the main menu → load a map → close GUI; mouse look must | ||
| 469 | work and the cursor must hide. Same sequence with alt-tab in the middle. | ||
| 470 | |||
| 471 | ### P2-6 Chams material state *(M)* | ||
| 472 | **Where:** `studio.cpp:267` (AlphaModulate set), `:282-291` (`chams_reset` | ||
| 473 | never resets it), `:350-384` (forced override cleared for every pass, | ||
| 474 | including shadow depth), `:494-510` (same `IMaterial*` refcounted 2–4×), | ||
| 475 | `:122-131` (`hook_draw_model` never restores the previous modulation). | ||
| 476 | |||
| 477 | **Fix:** save the previous material state (colour modulation, `$color`, | ||
| 478 | `$alpha`, `AlphaModulate`) and restore it in `chams_reset`; dedupe material | ||
| 479 | names in `chams_preload` (one `IncrementReferenceCount` per unique material); | ||
| 480 | skip the override when the current pass is a shadow-depth draw (compare the | ||
| 481 | engine's existing forced override, or check the draw flags before clearing); | ||
| 482 | restore `SetColorModulation` after `hook_draw_model`. | ||
| 483 | |||
| 484 | **Verify:** chams on/off repeatedly with a flashlight/shadow-casting map; | ||
| 485 | shadows must not disappear; translucent chams must not stay semi-transparent | ||
| 486 | after toggling off; static prop tint must not bleed onto other models. | ||
| 487 | |||
| 488 | ### P2-7 Font lifecycle *(S)* | ||
| 489 | **Where:** `gui.cpp:687-703` (create inside the family loop), | ||
| 490 | `:704-713`/`:725-727` (reset on level/scale change), `menu.cpp:1107-1131` | ||
| 491 | (same pattern). | ||
| 492 | |||
| 493 | **Fix:** create once per `(scale, DPI, font file)` and keep the handles; try | ||
| 494 | families by `AddGlyphSetToFont` on one handle where possible. Level changes | ||
| 495 | must not reset fonts at all (VGUI fonts are global). On scale change, | ||
| 496 | recreate once and log; accept the bounded leak because `ISurface` has no | ||
| 497 | `DestroyFont`. | ||
| 498 | |||
| 499 | **Verify:** die/respawn 20× and change UI scale 5×; log the `HFont` counter — | ||
| 500 | it must not grow per level change. | ||
| 501 | |||
| 502 | ### P2-8 Projection and resolution *(M)* | ||
| 503 | **Where:** `esp.cpp:57-79` (`project_manual` ignores the | ||
| 504 | aspect/(4/3) scaling Source applies at `view.cpp:1075-1083`, and ignores | ||
| 505 | roll), `:2078`, `:2925`, `:3301` (screen size cached once). | ||
| 506 | |||
| 507 | **Fix:** | ||
| 508 | 1. Prefer `IVDebugOverlay::ScreenPosition` everywhere; keep manual projection | ||
| 509 | only as a fallback, and in the fallback apply | ||
| 510 | `tanHalfH *= aspect / (4.0f/3.0f)` and include the roll term from | ||
| 511 | `g_cam_view.z`. | ||
| 512 | 2. Refresh `g_scr_w`/`g_scr_h` every frame (or on `GetScreenSize` change) — | ||
| 513 | mirror what `gui.cpp:928-930` already does. | ||
| 514 | 3. Don't mix `world_to_screen` and `project_manual` across one segment | ||
| 515 | (`esp.cpp:2006-2009`); pick one per segment. | ||
| 516 | |||
| 517 | **Verify:** 16:9, 16:10, 21:9, and windowed resize mid-session: blast rings and | ||
| 518 | trajectory arcs must stay glued to the world; no kink at the near plane. | ||
| 519 | |||
| 520 | ### P2-9 Skybox lifetime *(M)* | ||
| 521 | **Where:** `world.cpp:752-761` (in-place HDR linearisation), `:953-959` | ||
| 522 | (`skybox_invalidate` keeps texture + pixels), `:699-705`/`:720-729` | ||
| 523 | (originals stored without refcount), `:961-968` (`g_sky_tex = nullptr` leaks | ||
| 524 | the procedural texture). | ||
| 525 | |||
| 526 | **Fix:** keep a pristine copy (`g_sky_pixels_src`) and rebuild the working | ||
| 527 | buffer on each apply; `AddRef` originals when recorded and release on | ||
| 528 | invalidate; detach the regenerator and `DecrementReferenceCount` the old | ||
| 529 | procedural texture on reload. Either implement the documented | ||
| 530 | `SetUndefined()`/`Refresh()` path or update `CONTEXT.md:633-635` — right now | ||
| 531 | the docs describe code that does not exist. | ||
| 532 | |||
| 533 | **Verify:** HDR map → LDR map → HDR map; sky brightness must reset each time. | ||
| 534 | No material-system assert on map change with a custom skybox. | ||
| 535 | |||
| 536 | --- | ||
| 537 | |||
| 538 | ## 4. P3 — architecture | ||
| 539 | |||
| 540 | Do these only after P0/P1 are green. Each is behaviour-preserving and can land | ||
| 541 | independently. Order is by bug-surface reduction per unit of risk. | ||
| 542 | |||
| 543 | ### P3-1 Overlay layer: one ISurface wrapper *(M)* | ||
| 544 | `menu.cpp` (`SURF_*`/`surf_*`), `esp.cpp` (`SurfSlot`/`surf_*`), `gui.cpp` | ||
| 545 | (`GS_*`/`s_*`) each hardcode the same slots. Introduce `overlay.h/.cpp` with | ||
| 546 | the slot table, `surface_*` primitives, text drawing, clip handling, and the | ||
| 547 | `outer_draw()` dispatcher. Everything else calls it. This also gives P2-3/P2-1 | ||
| 548 | one place to fix. | ||
| 549 | |||
| 550 | ### P3-2 Declarative settings table *(M)* | ||
| 551 | Replace the current six-place edit (header extern, definition, | ||
| 552 | `config_save`, `config_load`, `config_reset_defaults`, `build_cards`) with one | ||
| 553 | table: | ||
| 554 | ```cpp | ||
| 555 | struct SettingDesc { const char* section; const char* key; Type type; | ||
| 556 | void* ptr; double def, min, max; const char* label; }; | ||
| 557 | ``` | ||
| 558 | Generate save/load/reset/clamp from it; `build_cards` consumes the same | ||
| 559 | descriptors. Eliminates P0-6 and the duplicated clamp bounds, and makes new | ||
| 560 | options cheap. | ||
| 561 | |||
| 562 | ### P3-3 Split `esp.cpp` *(L, mechanical)* | ||
| 563 | Target files, keeping current call graph: `entity_query.cpp` (class names, NPC | ||
| 564 | relation, viewmodel/owned-weapon helpers — also the home for `entity_dormant` | ||
| 565 | from P1-1), `combat.cpp` (aimbot/silent/triggerbot/melee/spread search), | ||
| 566 | `esp.cpp` (boxes/skeleton/arrows/speed graph/transitions), `items.cpp`, | ||
| 567 | `projectiles.cpp`. No behaviour change; move code verbatim, then fix includes. | ||
| 568 | |||
| 569 | ### P3-4 Session-reset registry *(S)* | ||
| 570 | One `session_reset()` called from the map-change path (`hl2h.cpp:784-793` | ||
| 571 | already has the hook point) that clears every file-scope cache: | ||
| 572 | `bones_clear`, `srv_checked`, `owned_mask`, `proj_first_seen`, bone ring | ||
| 573 | buffer, `g_scr_*`, spread state, aim/silent locks, item/proj arrays, | ||
| 574 | `g_map_last/prev`. Add a debug counter so a forgotten cache is visible. | ||
| 575 | |||
| 576 | ### P3-5 Header split and dependency direction *(M)* | ||
| 577 | Split `hl2h.h` into `common.h` / `core.h` / `settings.h` / `interfaces.h` / | ||
| 578 | `netvars.h` / `server.h` / `entity_query.h` / `bones.h` / `trace.h` / | ||
| 579 | `overlay.h` / `ui.h`. Enforce: features never include `hooks.h`/`init.h`; | ||
| 580 | `overlay.h` is the only place that knows ISurface slots; `common.h` depends on | ||
| 581 | nothing. This kills the star include graph and the circular | ||
| 582 | `hl2h.cpp ↔ menu.cpp` / `esp.cpp ↔ menu.cpp` / `studio.cpp ↔ esp.cpp` edges. | ||
| 583 | |||
| 584 | ### P3-6 Hook registry and teardown *(S)* | ||
| 585 | A small table of `{name, target, slot, original, replacement, installed}` with | ||
| 586 | `install_all()` / `restore_all()`, used by `hooks.cpp`. Enables the readiness | ||
| 587 | gate (P0-1) to be uniform and gives you an unload path for testing. | ||
| 588 | |||
| 589 | ### P3-7 Build and launch hygiene *(S)* | ||
| 590 | - `build.sh`: add `-pthread` (code uses `pthread_create`, | ||
| 591 | `std::mutex`, and links `-Wl,--no-undefined`); add object files + | ||
| 592 | dependency tracking for incremental builds; add an optional `-g`/`NDEBUG` | ||
| 593 | switch so crash addresses are symbolisable; fail loudly with a clear message | ||
| 594 | if `-m32` is unavailable; verify `assets/` is next to the built `.so`. | ||
| 595 | - `play.sh`: don't export `LD_PRELOAD` to the 64-bit Steam runtime wrapper | ||
| 596 | (every helper process warns about `ELFCLASS32`); set it only for the 32-bit | ||
| 597 | `hl2.sh` invocation, or re-exec the 32-bit binary directly. Move the | ||
| 598 | hardcoded `+map d2_coast_03` into a variable. | ||
| 599 | - Add `crash` symbolisation instructions (`addr2line -e libhl2hake.so`). | ||
| 600 | |||
| 601 | --- | ||
| 602 | |||
| 603 | ## 5. Testing strategy | ||
| 604 | |||
| 605 | The codebase has no tests, which is why most of these bugs are found by | ||
| 606 | inspection. Most of the P1/P2 defects live in pure functions; extract and test | ||
| 607 | them natively. | ||
| 608 | |||
| 609 | 1. **`tests/` with a 30-line assert harness**, compiled with the *host* | ||
| 610 | compiler (no `-m32`), no engine deps: | ||
| 611 | - config parser + `SettingDesc` clamping (P0-6, P3-2) | ||
| 612 | - `vector_angles` / `angle_to` / FOV math (P1-8) | ||
| 613 | - clip-rect and `text_in` truncation (P2-3) | ||
| 614 | - bone-cache eviction/validity policy as a pure `BoneCache` class | ||
| 615 | (P1-3) | ||
| 616 | - `proj_first_seen` ring (P1-9) | ||
| 617 | - seed selection / spread scoring (P1-6) | ||
| 618 | - `project_manual` aspect/roll (P2-8) | ||
| 619 | Compile with `-fsanitize=address,undefined`; run in CI-ish `make test`. | ||
| 620 | 2. **Hook-level assertions** (debug builds only): stage must be 3 before a | ||
| 621 | feature runs; `find_modules` only from the setup thread; `seq >= 0`; | ||
| 622 | `g_srv_*` only read after a successful resolve. | ||
| 623 | 3. **In-game smoke matrix** — run after every phase: | ||
| 624 | | Scenario | Covers | | ||
| 625 | |---|---| | ||
| 626 | | `+map` direct load, 10× | P0-1, P0-2 | | ||
| 627 | | main menu → load map → close GUI | P2-5 | | ||
| 628 | | death / respawn 20× | P2-7, P1-2 | | ||
| 629 | | map change A→B→A (HDR and LDR) | P1-2, P2-9, P3-4 | | ||
| 630 | | >64 NPCs on screen | P1-3 | | ||
| 631 | | resolution change + windowed resize | P2-8 | | ||
| 632 | | 60-min session | font/cache/leak growth | | ||
| 633 | | shotgun/pistol/crossbow strings | P1-5, P1-6 | | ||
| 634 | 4. **Regression corpus:** keep `crash`, plus `addr2line` output per fixed | ||
| 635 | item, so a reintroduced crash is recognisable. | ||
| 636 | |||
| 637 | --- | ||
| 638 | |||
| 639 | ## 6. Verified NOT a bug — do not fix | ||
| 640 | |||
| 641 | These appeared in the audit but did not survive verification. Listed so they | ||
| 642 | don't get "fixed" into new bugs. | ||
| 643 | |||
| 644 | | Claim | Reality | | ||
| 645 | |---|---| | ||
| 646 | | NPC `m_vecVelocity` read is OOB | Offset is `C_BaseEntity::m_vecVelocity` (`0xE0`), valid on all entities. Real issue is that NPCs don't network it → P1-10. | | ||
| 647 | | `CUtlVector` layout is wrong (`+12/+16`) | Correct for 32-bit (`utlvector.h:224-230`). Only range validation missing → P0-4. | | ||
| 648 | | Sky `memcpy` can overflow `dst` | Texture created at PNG dims, RGBA8888, so `pitch >= w*4 >= copy`. Add an assert, no fix. | | ||
| 649 | | `config_load` null deref is P0 | Real but only reachable from a hand-edited config (`config_save` never writes those keys). → P0-6, low. | | ||
| 650 | | `fog_enableskybox` stays on | `world.cpp:345-346` handles the `else` branch; the logic is correct. | | ||
| 651 | | Shotgun pellets need 48 `RandomFloat` draws | `RandomSeed` is called per pellet (`baseentity_shared.cpp:1699`), so 8 draws per pellet is enough. | | ||
| 652 | | Held `IN_ATTACK` breaks semi-autos | Source treats held `IN_ATTACK` as continuous fire; the concrete defect is the missing release edge for the pistol refire reset → folded into P1-5.5. | | ||
| 653 | | `bones_update` serves stale bones today | The missing `valid=false` is real but every in-tree caller checks the return. Fix anyway (P1-3) because it is one line. | | ||
| 654 | | `owned_mask` ORs forever | It resets when the local entity pointer changes; it is a same-map drop bug only. | | ||
| 655 | | `info_model_name` reads OOB | `name[64]` is followed by other header fields, so the 128-byte scan stays inside `studiohdr_t`. Correctness nit only (P0-8). | | ||
| 656 | | `draw_fov_circle` has no GUI toggle | Toggle exists (`gui.cpp:375`). Real bug is the `FEAT_ESP` gate → P1-11. | | ||
| 657 | | `%ebp` is safe because `-fno-omit-frame-pointer` | That flag only applies to this `.so`; callers are Valve binaries → P0-3. | | ||
| 658 | |||
| 659 | --- | ||
| 660 | |||
| 661 | ## 7. Sequencing summary | ||
| 662 | |||
| 663 | | Phase | Items | Effort | Gate to next phase | | ||
| 664 | |---|---|---|---| | ||
| 665 | | P0 | 8 | ~1.5 days | 10× direct-load and menu-load runs with no crash; netvars stable across map change | | ||
| 666 | | P1 | 11 | ~2.5 days | smoke matrix passes; aimbot/vischeck manual acceptance | | ||
| 667 | | P2 | 9 | ~2 days | visual acceptance (alpha, clipping, chams, skybox) + resize test | | ||
| 668 | | P3 | 7 | ~3 days | `tests/` green, `make` incremental, no behaviour diff on smoke matrix | | ||
| 669 | |||
| 670 | Quick wins if you only have an afternoon: **P0-1, P0-5, P0-6, P0-7, P0-8**, | ||
| 671 | plus **P2-2** (slider 10 px) and **P1-6** (spread gate). Those are all | ||
| 672 | small, local, and remove a crash class or a user-visible bug each. | ||
| 673 | |||
| 674 | --- | ||
| 675 | |||
| 676 | ## 8. Implementation log | ||
| 677 | |||
| 678 | ### Batch 1 — done, builds clean | ||
| 679 | |||
| 680 | | Item | Status | Notes | | ||
| 681 | |---|---|---| | ||
| 682 | | P0-1 hook readiness gate | done | `init_stage()`/`init_live()` with acquire/release; all hooks pass through until `INIT_LIVE`; setup order is now config+surface → input → client hooks. `INIT_FAILED` on every fatal setup path. | | ||
| 683 | | P0-2 module snapshots | done | `Module` globals replaced by `mod_client()`/`mod_engine()`/`mod_server()` returning an immutable, atomically published snapshot. Retired snapshots are intentionally leaked so a reader's reference never dangles. No lock on the read path. | | ||
| 684 | | P0-3 `%ebp` walk | hardened | New `caller_self_arg()` validates that the walk stays inside the current stack (above `%esp`, <1 MiB, 4-aligned) before dereferencing. Still frame-pointer based; a future pass should drop it in favour of arming from `CreateMove` (option 1 in P0-3). Spread window now reset at every `do_move`. | | ||
| 685 | | P0-4 netvar bounds | done | `module_range(p, n)` added; every `RecvTable`/`RecvProp`/string/array dereference is validated before use. | | ||
| 686 | | P0-5 vtable header | done | `vtable_clone()` preserves `[-2]/[-1]`; used by client, CInput, IVModelRender and IStudioRender hooks. | | ||
| 687 | | P0-6 config `cur` | done | Stray `active=`/`bind=` in scalar/chams sections is ignored instead of dereferenced. | | ||
| 688 | | P0-7 signed `seq` | done | `seq < 0` rejected; modulo uses `unsigned`. | | ||
| 689 | | P0-8 defensive batch | partly | Bone parent-cycle guard, `mprotect` return check and no `PROT_EXEC`, `server.so` `.bss` tail absorbed, studio name bounded to 64. Not done: `build_core_mask` caching. | | ||
| 690 | | P1-6 spread gate | done | Seed hook now arms on `no_spread` only; `engine` toggle controls only the `RandomFloat` zeroing. | | ||
| 691 | | P2-2 slider geometry | done | `slider_track()` is the single source of truth for draw, hit-test and drag; drag clamps `t` to `[0,1]`. | | ||
| 692 | |||
| 693 | Verification for this batch is compile-level (`build.sh`, `--no-undefined`) | ||
| 694 | plus a diff review. In-game runs still needed for P0-1/P0-2/P0-3 as described | ||
| 695 | in their own sections. | ||
| 696 | |||
| 697 | ### Batch 2 — done, builds clean | ||
| 698 | |||
| 699 | | Item | Status | Notes | | ||
| 700 | |---|---|---| | ||
| 701 | | P1-1 dormant entities | done | `entity_is_dormant()` on `IClientNetworkable` slot 8, applied in esp_collect, aimbot candidate scan, the NPC visibility helper, melee, item ESP and projectile ESP. Slot layout is self-tested against the local player (which is never dormant): a wrong slot disables the check with a log line instead of misreporting every entity. | | ||
| 702 | | P1-2 `srv_checked` latch | done | Moved to file scope, exposed via `esp_reset_session()` called on local-entity change; only latches once both client and server model indices are valid. | | ||
| 703 | | P1-3 bone cache (part) | partly | `e->valid = false` now set on the two early returns that skipped it. Still to do: cache sizing/eviction and the `head_position` early-return. | | ||
| 704 | | P1-4 vischeck | done | Glass stepping advances until a probe is clear (doubling, capped, never past the target) instead of a fixed 0.5 units; `VisibilityFilter` ignores entities owned by the target via `m_hOwnerEntity`. | | ||
| 705 | | P1-5 aimbot | part | `maxs < 0` no longer disables the aimbot; prefilter widens the FOV test by the AABB angular radius; lock uses 1.5× hysteresis instead of no limit; the projectile lead point is re-checked for FOV and visibility and falls back to the un-led point. Still to do: `IN_ATTACK` release edge. | | ||
| 706 | | P1-7 owner netvars | done | `m_hOwnerEntity` / `m_hOwner` now resolved (previously always −1). | | ||
| 707 | | P1-8 math/movement | part | `vector_angles` pitch no longer wraps to `[270,360)`; `point_ground` requires `normal.z >= 0.7`; underwater jump+duck no longer skips combat (`goto bhop_done`); melee AABB rejects hugely negative `mins`. Still to do: triggerbot `entindex()`. | | ||
| 708 | | P1-9 fuse table | done | 64-entry ring with stale-entry reset; no longer freezes fuse time after 32 projectiles. | | ||
| 709 | | P1-11 FOV circle | done | Moved out of `esp_draw` into `outer_draw`, gated on aimbot/silent-aim being active plus the toggle, with its own screen-size init. | | ||
| 710 | | P2-1 draw once per frame | part | `outer_draw()` now has a 1 ms gate (panels within a frame are microseconds apart) and `DrawPrintText` only acts as a fallback when no paint happened for 50 ms. Still to do: save/restore ISurface text state. | | ||
| 711 | |||
| 712 | ### Batch 3 — done, builds clean | ||
| 713 | |||
| 714 | | Item | Status | Notes | | ||
| 715 | |---|---|---| | ||
| 716 | | P2-3 clipping | done | `line()` is now Liang–Barsky clipped and used by checkbox checkmarks and picker cursors; `outline()` draws four clipped edges instead of outlining the clamped rect; `text_in()` truncates with `...` at `max_w`/clip edge and no longer shifts left. `s_outline` removed. | | ||
| 717 | | P2-4 pointer discipline | done | Clicking outside an open dropdown consumes the click (no activation of the control underneath); `handle_hover` no longer arms hidden sliders while a dropdown, picker, or drag is active. | | ||
| 718 | | P2-5 SDL state | part | `sd_*` pointers now resolve with `RTLD_NEXT` instead of `RTLD_DEFAULT` (which found this file's own wrapper); the wrappers record what the game last requested and `close_gui()` restores that instead of the pre-open snapshot. Not changed: dropping `SDL_MOUSEMOTION` while open. | | ||
| 719 | | P2-6 chams | part | `chams_reset()` now resets `AlphaModulate` (shared materials were permanently left half-faded); `chams_preload()` references each unique material once; prop tint restores white after `DrawModel`/`DrawModelStaticProp`. Not addressed: clearing `ForcedMaterialOverride` during shadow-depth passes (needs a way to read the engine's current override). | | ||
| 720 | | P2-7 fonts | part | `create_gui_font()` creates one handle and adds glyph sets to it instead of leaking one per failed family; `ensure_fonts()` keeps valid handles across level changes and only recreates when `font_valid()` fails. `menu.cpp`'s own font pair still resets on local-entity change. | | ||
| 721 | | P2-8 projection | done | `project_manual()` applies Source's 4:3-reference aspect scaling and includes roll; screen size is refreshed every frame through `esp_update_screen()` in all four draw paths. | | ||
| 722 | | P2-9 skybox | part | Pristine `g_sky_pixels_src` so HDR linearisation no longer bleeds into a later LDR map; `skybox_reload_image()` detaches the regenerator before dropping the handle. Original `ITexture*` refcounts still not taken (needs the ITexture slot verified against the shipped binary). | | ||
| 723 | | P3-4 session reset | done | One `session_reset()` called on local-entity change, listing every per-map cache. | | ||
| 724 | | P3-7 build | done | `-pthread`; incremental objects in `build/{release,debug}` (verified: second run compiles nothing); `DEBUG=1` for `-O0 -g`; explicit 32-bit toolchain check; assets warning; `build/` ignored. | | ||
| 725 | |||
| 726 | Self-review during this batch caught one bug in my own P1-1 change: the | ||
| 727 | dormant self-test could run before a local player existed (main menu) and latch | ||
| 728 | "disabled" permanently. It now leaves the state untested until a local player | ||
| 729 | is available. | ||
| 730 | |||
| 731 | ### In-game validation | ||
| 732 | |||
| 733 | Batch 1–3 items were exercised by the user in a live game and all behaved | ||
| 734 | correctly. Two follow-ups were found and fixed: | ||
| 735 | |||
| 736 | - speed-graph background flicker — the 1 ms draw gate was not a frame boundary | ||
| 737 | (see the P2-1 follow-up below); | ||
| 738 | - FOV circle never visible — it was gated on `feature_active()`, which requires | ||
| 739 | the bind (default mouse5) to be held; now uses `feature_enabled()`. | ||
| 740 | |||
| 741 | Both confirmed fixed. | ||
| 742 | |||
| 743 | ### Batch 4 — done, builds clean | ||
| 744 | |||
| 745 | | Item | Status | Notes | | ||
| 746 | |---|---|---| | ||
| 747 | | P3-1 overlay layer | done | New `overlay.h` / `overlay.cpp` own the VGUI_Surface030 slot table and every ISurface call. `g_surface` moved there; `menu.cpp`, `gui.cpp` and `esp.cpp` keep their local wrapper names as one-line forwarders, so no call sites changed. Three duplicated slot tables are gone. | | ||
| 748 | | P1-3 bone cache | done | Cache raised to 192 entries (128 targets + room for failed updates); `head_position()` continues past a name-matched bone whose matrix is invalid instead of aborting the search. | | ||
| 749 | | P1-5 fire control | done | `fire_edge_ok()` alternates press/release for semi-auto weapons (pistol, .357, crossbow, shotgun, RPG, gravity gun, crowbar) and holds the button for automatic ones. Applied to silent aim, melee/gravity and triggerbot. | | ||
| 750 | | P1-10 NPC lead | done | New `server_entity_velocity()` reads the listen-server `m_vecVelocity` (already resolved but unused); `projectile_lead_point()` prefers it and falls back to the client field. | | ||
| 751 | | P2-9 `ITexture` refcounts | done | `record_orig()` now takes a reference (slot 10) on every captured texture and `release_sky_originals()` drops them (slot 11) wherever the list is cleared, so a map change can no longer leave `g_sky_orig` holding freed pointers. Slot numbers follow the material-system header order already verified here (SetTextureRegenerator = 12, Download = 13). | | ||
| 752 | | P2-5 motion events | done | `SDL_PollEvent` now keeps `SDL_MOUSEMOTION` events and zeroes only `xrel`/`yrel`, instead of dropping the event entirely (VGUI hover and SDL cursor state stay consistent). | | ||
| 753 | | P1-11 follow-up (circle) | done | The circle was gated on `feature_active()`, which requires the bind to be held; the default aimbot bind is mouse5, so it only appeared while aiming. `feature_enabled()` (toggle only) is now used, and the circle is skipped when there is no local player (main menu). | | ||
| 754 | | P2-1 follow-up (flicker) | done | The 1 ms time gate was the wrong frame boundary: panels more than 1 ms apart in one frame drew twice, stacking the 120-alpha speed-graph background, so it alternated brightness. `overlay_frame_mark()` is now called from `FRAME_RENDER_START` and `overlay_should_draw()` allows exactly one draw per engine frame, with a 10 ms fallback for menu/pause frames that never send the stage. | | ||
| 755 | |||
| 756 | ### Performance pass | ||
| 757 | |||
| 758 | The audit's hot paths were removed rather than micro-optimised: | ||
| 759 | |||
| 760 | | Cost | Before | After | | ||
| 761 | |---|---|---| | ||
| 762 | | NPC classification from the chams hook | `esp_npc_relation()` did `esp_is_npc_index` → entity/model lookup → `server_npc_relation` (edict + `CUtlVector` scan) + `server_npc_state` per model draw, up to 3 passes per entity per frame | 50 ms TTL cache keyed by entity index; a draw now costs a hash lookup | | ||
| 763 | | Friendly/neutral test | `server_npc_relation` + `server_npc_state` (+ turret flags) per call, from the entity scan, aimbot selection and chams | 100 ms TTL cache | | ||
| 764 | | Damage class | `server_npc_takedamage` (edict lookup) + ~20 `strcasestr` per NPC per scan | 250 ms TTL cache | | ||
| 765 | | Model name | two `IVModelInfo` virtual calls per entity per scan (~1000 entities at 100 Hz) | memoised by model index, cleared on map change | | ||
| 766 | | Triggerbot index recovery | walked up to 4096 entity-list slots per fire tick | direct-mapped entity-pointer → index cache filled by the entity scan; the scan remains only as a cold-miss fallback | | ||
| 767 | |||
| 768 | Item and projectile markers were the other half of the problem: discovery | ||
| 769 | (`items_collect` / `projs_collect`) is throttled to 10 Hz because it walks the | ||
| 770 | entity list, but both draw paths already stored the entity index. They now | ||
| 771 | refresh the origin from the live netvar per frame (`entity_origin()`), so | ||
| 772 | markers, blast-radius rings and the grenade trajectory follow the world at | ||
| 773 | render rate while the expensive scan stays at 10 Hz. The trajectory also uses | ||
| 774 | the networked `m_DmgRadius` instead of a hardcoded 300, and its landing ring | ||
| 775 | respects the "Blast radius" toggle. | ||
| 776 | |||
| 777 | Caches are cleared by `esp_reset_session()` on map change/respawn. The ESP scan | ||
| 778 | itself is unchanged: projection only happens for the handful of entities that | ||
| 779 | survive the class/life/friendly filters, not for every entity. | ||
| 780 | |||
| 781 | ### Known remaining work | ||
| 782 | |||
| 783 | Deliberately deferred, with reasons: | ||
| 784 | |||
| 785 | - **P1-8 triggerbot `entindex()`** — the current code is correct, only O(N) | ||
| 786 | per fire tick. Replacing it needs a verified `IClientNetworkable::entindex` | ||
| 787 | slot plus a way to get the networkable from a raw entity pointer, or a | ||
| 788 | cached pointer→index map. Not worth the risk for a hot path that fires a few | ||
| 789 | times per second. | ||
| 790 | - **P2-6 shadow-depth `ForcedMaterialOverride`** — requires reading the | ||
| 791 | engine's current override, which VEngineModel016 does not expose. Needs an | ||
| 792 | alternative detection (draw flags in `DrawModelState_t`) validated in-game. | ||
| 793 | - **P3-2 declarative settings table** — large rewrite of `config_save`, | ||
| 794 | `config_load`, `config_reset_defaults` and `build_cards`. It can regress | ||
| 795 | config persistence, so it should follow an in-game pass. | ||
| 796 | - **P3-3 `esp.cpp` split**, **P3-5 header split**, **P3-6 hook registry** — | ||
| 797 | behaviour-neutral moves with a large diff; best done after the current build | ||
| 798 | has been validated in-game so a regression is easy to bisect. | ||
| 799 | |||
| 800 | Nothing in the two batches has been exercised in-game yet. The smoke matrix in | ||
| 801 | §5 is the next step: direct map load 10× (P0-1), menu → map → GUI close (P2-5), | ||
| 802 | map change A→B→A (P1-2/P2-9), >64 NPCs (P1-3), and a 60-minute session. | ||
