summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-06 16:09:55 +0100
committersalmonik <salmonik@tuta.io>2026-11-06 16:09:55 +0100
commit33f5ed9ef58f26c6b3b52cf605dd4311d658a60d (patch)
treea97254872e465c9107b7ef19695ab56bb076d7e2
parentItems: log the usable ammo table once (diff)
downloadhl2h-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
-rw-r--r--.gitignore1
-rw-r--r--FIX_PLAN.md802
-rw-r--r--Makefile60
-rwxr-xr-xbuild.sh16
-rw-r--r--src/bones.cpp36
-rw-r--r--src/cinput.cpp39
-rw-r--r--src/esp.cpp582
-rw-r--r--src/gui.cpp259
-rw-r--r--src/hl2h.cpp214
-rw-r--r--src/hl2h.h50
-rw-r--r--src/menu.cpp99
-rw-r--r--src/modules.cpp47
-rw-r--r--src/netvars.cpp50
-rw-r--r--src/overlay.cpp140
-rw-r--r--src/overlay.h62
-rw-r--r--src/server.cpp27
-rw-r--r--src/studio.cpp71
-rw-r--r--src/vischeck.cpp62
-rw-r--r--src/world.cpp45
19 files changed, 2217 insertions, 445 deletions
diff --git a/.gitignore b/.gitignore
index 1239075..44e3233 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
1libhl2hake.so 1libhl2hake.so
2a.out 2a.out
3crash 3crash
4build/
4# vendored SDK + reference cheats (read-only material) 5# vendored SDK + reference cheats (read-only material)
5source-sdk-2013/ 6source-sdk-2013/
6Amalgam/ 7Amalgam/
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
3Derived from a full audit of `src/` (12,243 lines) plus `build.sh` / `play.sh`.
4Every item below was re-verified against the working tree; claims that did not
5survive verification are listed in [§6](#6-verified-not-a-bug--do-not-fix) so
6nobody wastes time on them.
7
8Ordering: **P0 → P1 → P2 → P3**. Each phase leaves the cheat playable.
9Work inside a phase is independent enough to parallelise; cross-phase
10dependencies 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
29These are the items that produce the existing `crash` log and the highest risk
30of 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
38and ISurface discovery. The game thread can enter `hkdCreateMove` /
39`hkdInputCreateMove` / `hkdModelExecute` while `setup_thread` is still writing
40feature globals and `g_surface`. Every hook is also installed lazily from
41different threads (surface hooks from `CreateMove`, studio-prop hooks from the
42paint path), so "is init done" is not observable.
43
44**Fix:**
451. Add `std::atomic<int> g_init_stage {0}` (`0` = nothing, `1` = interfaces +
46 netvars, `2` = config + surface + fonts, `3` = hooks live).
472. Move `install_hook()`, `install_input_hook()`, and the seed/uniform patches
48 to *after* `install_menu()` completes.
493. 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.
524. Keep lazy surface/studio installs, but have them check the stage instead of
53 relying on "first `CreateMove` implies ready".
545. 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,
5810× 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`
67calls `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`,
71freeing `std::vector<Range>` and `std::string` buffers while another thread
72iterates them. Locking only the writer is worse than no lock — it hides the
73race.
74
75**Fix:**
761. 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).
802. 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.
823. 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
89practical; otherwise add an assertion that `find_modules` is only called from
90the 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
97only 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
101standard frame chain and a first-argument-at-`+8` layout in code we do not
102control. A non-frame-pointer caller, tail call, or different convention
103segfaults immediately.
104
105**Fix (in order of preference):**
1061. 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.
1122. 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.
1163. 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
121crash 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
132bool module_range(uintptr_t p, size_t n); // p and p+n-1 inside one range
133```
134Use it for every pointer before the first dereference and for every string
135scan (`module_range(p, outn)`), then keep the existing printable-char check.
136Add 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
140map 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;`
149so `new[-1]` (typeinfo) and `new[-2]` (offset-to-top) are heap metadata.
150The cheat never calls RTTI on these pointers, so this is currently latent, but
151the 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
154slots 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
156also removes the duplicated 256/32/49 magic sizes.
157
158**Verify:** hooks still fire (`logd` in each handler); no behaviour change
159expected. 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
168hand-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
172key 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
184huge index. The `command_number` sanity check happens *after* the pointer is
185formed 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)*
196Small, 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
221aimbot/triggerbot/melee this is a correctness fix (stale frozen positions);
222for 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
227tick, aimbot stops acquiring it, and no `schema`/studio asserts appear in
228console.
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
235temporary `esp_reset_session()` called from `hl2h.cpp:784` is enough now), and
236only latch when `cli_mi > 0 && srv_mi > 0`. Keep re-checking per NPC for the
237first ~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:**
2481. 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.
2512. Set `e->valid = false` on *every* failure path, including `:322-330`.
2523. `head_position`: `continue` instead of `return` when `hitbox_world` /
253 `bone_pos` fails, so the fallback chain still runs.
2544. 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
258must 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:**
2671. 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`.
2712. `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`.
2743. 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
278gunship hitboxes, and a target holding a weapon — ESP visibility and aimbot
279vischeck 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:**
2881. Delete `g_net.maxs < 0` from the gate at `:815`; rely on the
289 `maxz = ... : 72.0f` fallback.
2902. 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`.
2943. 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.
2984. 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.
3015. 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
30710-shot strings at short/medium/long range, log chosen target id, hitbox, and
308whether 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
313requires `no_spread`.
314
315**Problem:** turning off "Engine spread override" disables the seed hook
316entirely, 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
324be 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),
332then 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;
336vischeck 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
349steep-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
353eviction); `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`
357from `server_player_weapon(local_idx, slot)` on every scan when the server
358interface is available (keep the OR as fallback only).
359
360**Verify:** throw >40 grenades on one map; fuse timers must stay correct. Drop
361and 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
365targets, 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
374should 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
388with 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
396prevents 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`
401only 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
412whether ESP is drawn once or with the old code; instrument a frame counter and
413assert 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
423drag 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
426track 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
436through the card, no checkmark floating in the header, long dropdown labels
437truncate 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
444closes, return immediately. `handle_hover` early-outs while a dropdown, colour
445picker drag, or slider drag is active. Also close pickers/dropdowns on
446outside-click consistently (currently pickers need another colour control).
447
448**Verify:** open a dropdown over a toggle row and click the toggle; the
449dropdown 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
453own exported wrapper), `:2415-2434` (requests dropped while open),
454`:1382-1385` (restores the pre-open snapshot), `:1410-1412` (snapshot).
455
456**Fix:**
4571. 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).
4602. 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.
4633. 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".
4654. 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
469work 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`
473never resets it), `:350-384` (forced override cleared for every pass,
474including 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
479names in `chams_preload` (one `IncrementReferenceCount` per unique material);
480skip the override when the current pass is a shadow-depth draw (compare the
481engine's existing forced override, or check the draw flags before clearing);
482restore `SetColorModulation` after `hook_draw_model`.
483
484**Verify:** chams on/off repeatedly with a flashlight/shadow-casting map;
485shadows must not disappear; translucent chams must not stay semi-transparent
486after 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
494families by `AddGlyphSetToFont` on one handle where possible. Level changes
495must not reset fonts at all (VGUI fonts are global). On scale change,
496recreate 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 —
500it must not grow per level change.
501
502### P2-8 Projection and resolution *(M)*
503**Where:** `esp.cpp:57-79` (`project_manual` ignores the
504aspect/(4/3) scaling Source applies at `view.cpp:1075-1083`, and ignores
505roll), `:2078`, `:2925`, `:3301` (screen size cached once).
506
507**Fix:**
5081. 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`.
5122. Refresh `g_scr_w`/`g_scr_h` every frame (or on `GetScreenSize` change) —
513 mirror what `gui.cpp:928-930` already does.
5143. 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
518trajectory 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
524the procedural texture).
525
526**Fix:** keep a pristine copy (`g_sky_pixels_src`) and rebuild the working
527buffer on each apply; `AddRef` originals when recorded and release on
528invalidate; detach the regenerator and `DecrementReferenceCount` the old
529procedural texture on reload. Either implement the documented
530`SetUndefined()`/`Refresh()` path or update `CONTEXT.md:633-635` — right now
531the docs describe code that does not exist.
532
533**Verify:** HDR map → LDR map → HDR map; sky brightness must reset each time.
534No material-system assert on map change with a custom skybox.
535
536---
537
538## 4. P3 — architecture
539
540Do these only after P0/P1 are green. Each is behaviour-preserving and can land
541independently. 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
546the 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
548one place to fix.
549
550### P3-2 Declarative settings table *(M)*
551Replace the current six-place edit (header extern, definition,
552`config_save`, `config_load`, `config_reset_defaults`, `build_cards`) with one
553table:
554```cpp
555struct SettingDesc { const char* section; const char* key; Type type;
556 void* ptr; double def, min, max; const char* label; };
557```
558Generate save/load/reset/clamp from it; `build_cards` consumes the same
559descriptors. Eliminates P0-6 and the duplicated clamp bounds, and makes new
560options cheap.
561
562### P3-3 Split `esp.cpp` *(L, mechanical)*
563Target files, keeping current call graph: `entity_query.cpp` (class names, NPC
564relation, viewmodel/owned-weapon helpers — also the home for `entity_dormant`
565from 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)*
570One `session_reset()` called from the map-change path (`hl2h.cpp:784-793`
571already has the hook point) that clears every file-scope cache:
572`bones_clear`, `srv_checked`, `owned_mask`, `proj_first_seen`, bone ring
573buffer, `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)*
577Split `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
581nothing. 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)*
585A small table of `{name, target, slot, original, replacement, installed}` with
586`install_all()` / `restore_all()`, used by `hooks.cpp`. Enables the readiness
587gate (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
605The codebase has no tests, which is why most of these bugs are found by
606inspection. Most of the P1/P2 defects live in pure functions; extract and test
607them natively.
608
6091. **`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`.
6202. **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.
6233. **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 |
6344. **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
641These appeared in the audit but did not survive verification. Listed so they
642don'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
670Quick wins if you only have an afternoon: **P0-1, P0-5, P0-6, P0-7, P0-8**,
671plus **P2-2** (slider 10 px) and **P1-6** (spread gate). Those are all
672small, 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
693Verification for this batch is compile-level (`build.sh`, `--no-undefined`)
694plus a diff review. In-game runs still needed for P0-1/P0-2/P0-3 as described
695in 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
726Self-review during this batch caught one bug in my own P1-1 change: the
727dormant 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
729is available.
730
731### In-game validation
732
733Batch 1–3 items were exercised by the user in a live game and all behaved
734correctly. 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
741Both 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
758The 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
768Item 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
770entity list, but both draw paths already stored the entity index. They now
771refresh the origin from the live netvar per frame (`entity_origin()`), so
772markers, blast-radius rings and the grenade trajectory follow the world at
773render rate while the expensive scan stays at 10 Hz. The trajectory also uses
774the networked `m_DmgRadius` instead of a hardcoded 300, and its landing ring
775respects the "Blast radius" toggle.
776
777Caches are cleared by `esp_reset_session()` on map change/respawn. The ESP scan
778itself is unchanged: projection only happens for the handful of entities that
779survive the class/life/friendly filters, not for every entity.
780
781### Known remaining work
782
783Deliberately 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
800Nothing 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),
802map change A→B→A (P1-2/P2-9), >64 NPCs (P1-3), and a 60-minute session.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..12a6f92
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,60 @@
1# libhl2hake.so — 32-bit Source Engine plugin.
2#
3# make release build (build/release, -O2)
4# make debug build/debug with -O0 -g (symbolise crash addresses)
5# make clean
6#
7# Needs a 32-bit multilib toolchain (g++-multilib on Debian/Ubuntu).
8
9CXX ?= g++
10OUT := libhl2hake.so
11SRC := $(wildcard src/*.cpp)
12ODIR ?= build/release
13OPT ?= -O2
14
15CXXFLAGS := -m32 -std=c++17 -Wall -Wextra -Werror -fPIC \
16 -fvisibility=hidden -fno-omit-frame-pointer -pthread \
17 -MMD -MP $(OPT)
18LDFLAGS := -m32 -shared -pthread -Wl,--no-undefined
19LDLIBS := -ldl
20
21OBJ := $(patsubst src/%.cpp,$(ODIR)/%.o,$(SRC))
22DEP := $(OBJ:.o=.d)
23
24all: check $(OUT)
25 @file $(OUT)
26 @test -f assets/LiberationSans-Regular.ttf || \
27 echo "warning: assets/ is missing next to the .so; the overlay falls back to system fonts" >&2
28
29# Objects, dependency files and headers are handled by make itself; this only
30# keeps the debug build in its own directory.
31debug:
32 @$(MAKE) --no-print-directory ODIR=build/debug OPT="-O0 -g"
33
34# Always relink. Both configs write the same output file, so without this a
35# plain `make` after `make debug` would keep the debug binary (the .so is newer
36# than the release objects). Linking is sub-second.
37.PHONY: FORCE
38FORCE:
39
40$(OUT): $(OBJ) FORCE
41 $(CXX) $(LDFLAGS) -o $@ $(OBJ) $(LDLIBS)
42
43$(ODIR)/%.o: src/%.cpp | $(ODIR)
44 $(CXX) $(CXXFLAGS) -c $< -o $@
45
46$(ODIR):
47 @mkdir -p $@
48
49# Fail once with a usable message instead of a wall of compiler errors.
50.PHONY: check
51check:
52 @printf 'int main(){return 0;}\n' | \
53 $(CXX) -m32 -x c++ - -o /dev/null 2>/dev/null || \
54 { echo "error: $(CXX) cannot build 32-bit binaries; install g++-multilib" >&2; exit 1; }
55
56.PHONY: all debug clean
57clean:
58 rm -rf build $(OUT)
59
60-include $(DEP)
diff --git a/build.sh b/build.sh
index b12f67b..4d10a56 100755
--- a/build.sh
+++ b/build.sh
@@ -1,8 +1,10 @@
1#!/bin/sh 1#!/bin/sh
2set -e 2# The build is defined in Makefile. This wrapper exists only so the old
3cd "$(dirname "$0")" 3# ./build.sh invocation (and the docs that mention it) keep working.
4g++ -m32 -O2 -std=c++17 -Wall -Wextra -Werror -fPIC -shared -fvisibility=hidden -fno-omit-frame-pointer \ 4#
5 -o libhl2hake.so \ 5# ./build.sh -> make
6 src/hl2h.cpp src/modules.cpp src/cinput.cpp src/netvars.cpp src/menu.cpp src/gui.cpp src/esp.cpp src/server.cpp src/bones.cpp src/vischeck.cpp src/world.cpp src/studio.cpp \ 6# DEBUG=1 ./build.sh -> make debug
7 -ldl -Wl,--no-undefined 7if [ "${DEBUG:-0}" = "1" ]; then
8file libhl2hake.so 8 exec make --no-print-directory debug "$@"
9fi
10exec make --no-print-directory "$@"
diff --git a/src/bones.cpp b/src/bones.cpp
index c777067..c4168a9 100644
--- a/src/bones.cpp
+++ b/src/bones.cpp
@@ -100,7 +100,10 @@ struct BoneEntry {
100 Matrix3x4 mats[MAX_BONES]; 100 Matrix3x4 mats[MAX_BONES];
101}; 101};
102 102
103static BoneEntry g_entries[64]; 103// Sized for the 128-target scan with headroom: failed updates also allocate an
104// entry, and 64 slots evicted live NPCs mid-frame.
105constexpr int BONE_CACHE = 192;
106static BoneEntry g_entries[BONE_CACHE];
104static int g_next = 0; 107static int g_next = 0;
105static int g_logged = 0; 108static int g_logged = 0;
106static bool g_renderable_warned = false; 109static bool g_renderable_warned = false;
@@ -112,14 +115,14 @@ static double bone_now() {
112} 115}
113 116
114static BoneEntry *find_entry(int idx) { 117static BoneEntry *find_entry(int idx) {
115 for (int i = 0; i < 64; i++) 118 for (int i = 0; i < BONE_CACHE; i++)
116 if (g_entries[i].ent && g_entries[i].idx == idx) 119 if (g_entries[i].ent && g_entries[i].idx == idx)
117 return &g_entries[i]; 120 return &g_entries[i];
118 return nullptr; 121 return nullptr;
119} 122}
120 123
121static BoneEntry *alloc_entry(int idx, void *ent) { 124static BoneEntry *alloc_entry(int idx, void *ent) {
122 BoneEntry *e = &g_entries[g_next++ % 64]; 125 BoneEntry *e = &g_entries[g_next++ % BONE_CACHE];
123 memset(e, 0, sizeof(*e)); 126 memset(e, 0, sizeof(*e));
124 e->idx = idx; 127 e->idx = idx;
125 e->ent = ent; 128 e->ent = ent;
@@ -321,13 +324,17 @@ bool bones_update(int idx, void *ent) {
321 324
322 const void *expected = 325 const void *expected =
323 vfunc<GetModelIdxFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); 326 vfunc<GetModelIdxFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex);
324 if (!expected) 327 if (!expected) {
328 e->valid = false;
325 return false; 329 return false;
330 }
326 331
327 // IClientUnknown::GetClientRenderable = primary vtable slot 6. 332 // IClientUnknown::GetClientRenderable = primary vtable slot 6.
328 void *r = vfunc<GetRenderableFn>(ent, 6)(ent); 333 void *r = vfunc<GetRenderableFn>(ent, 6)(ent);
329 if (!r) 334 if (!r) {
335 e->valid = false;
330 return false; 336 return false;
337 }
331 // Validate the interface before using SetupBones on it. 338 // Validate the interface before using SetupBones on it.
332 const void *rmodel = vfunc<GetModelFn>(r, 9)(r); 339 const void *rmodel = vfunc<GetModelFn>(r, 9)(r);
333 if (rmodel != expected) { 340 if (rmodel != expected) {
@@ -418,9 +425,14 @@ int bone_nearest_core_parent(int idx, int bone) {
418 return -1; 425 return -1;
419 int p = bone_at(e->hdr, bone)->parent; 426 int p = bone_at(e->hdr, bone)->parent;
420 if (!e->humanoid) { 427 if (!e->humanoid) {
421 while (p >= 0 && p < e->count && !e->bone_valid[p]) 428 // Same cycle guard as the core path: a malformed model can self-parent a
429 // bone whose matrix is invalid, which would spin here forever.
430 for (int guard = 0; p >= 0 && p < e->count && guard < 64; guard++) {
431 if (e->bone_valid[p])
432 return p;
422 p = bone_at(e->hdr, p)->parent; 433 p = bone_at(e->hdr, p)->parent;
423 return (p >= 0 && p < e->count) ? p : -1; 434 }
435 return -1;
424 } 436 }
425 for (int guard = 0; p >= 0 && p < e->count && guard < 64; guard++) { 437 for (int guard = 0; p >= 0 && p < e->count && guard < 64; guard++) {
426 if (e->core[p] && e->bone_valid[p]) 438 if (e->core[p] && e->bone_valid[p])
@@ -487,14 +499,16 @@ bool head_position(int idx, Vector &out) {
487 if (boxes[i].bone < 0 || boxes[i].bone >= e->count) 499 if (boxes[i].bone < 0 || boxes[i].bone >= e->count)
488 continue; 500 continue;
489 const char *bn = bone_name_raw(e->hdr, boxes[i].bone); 501 const char *bn = bone_name_raw(e->hdr, boxes[i].bone);
490 if (bn && head_like_name(bn)) 502 // Keep looking: the first name match can be a bone whose matrix failed
491 return hitbox_world(e, boxes[i], out); 503 // validation, and returning its false result aborted the whole search.
504 if (bn && head_like_name(bn) && hitbox_world(e, boxes[i], out))
505 return true;
492 } 506 }
493 } 507 }
494 for (int i = 0; i < e->count; i++) { 508 for (int i = 0; i < e->count; i++) {
495 const char *bn = bone_name_raw(e->hdr, i); 509 const char *bn = bone_name_raw(e->hdr, i);
496 if (bn && head_like_name(bn)) 510 if (bn && head_like_name(bn) && bone_pos(idx, i, out))
497 return bone_pos(idx, i, out); 511 return true;
498 } 512 }
499 // Last resort for non-humanoid models with unnamed heads/hitboxes: the 513 // Last resort for non-humanoid models with unnamed heads/hitboxes: the
500 // first hitbox is the body (still a valid aim point). 514 // first hitbox is the body (still a valid aim point).
diff --git a/src/cinput.cpp b/src/cinput.cpp
index 08686a1..45e2b04 100644
--- a/src/cinput.cpp
+++ b/src/cinput.cpp
@@ -53,23 +53,25 @@ static constexpr size_t VERIFIED_STRIDE = 0x58;
53static constexpr size_t VERIFIED_CRC_OFF = 0x54; 53static constexpr size_t VERIFIED_CRC_OFF = 0x54;
54 54
55CUserCmd *get_usercmd(int seq) { 55CUserCmd *get_usercmd(int seq) {
56 if (!g_input) 56 // Negative sequence numbers would make `seq % 90` negative and the size_t
57 // cast a huge index; reject before forming the pointer.
58 if (!g_input || seq < 0)
57 return nullptr; 59 return nullptr;
58 uintptr_t cmds = *(uintptr_t *)(g_input + CMDOFF_M_PCOMMANDS); 60 uintptr_t cmds = *(uintptr_t *)(g_input + CMDOFF_M_PCOMMANDS);
59 if (!cmds) 61 if (!cmds)
60 return nullptr; 62 return nullptr;
61 auto *cmd = (CUserCmd *)(cmds + (size_t)(seq % 90) * 0x54); 63 auto *cmd = (CUserCmd *)(cmds + (size_t)((unsigned)seq % 90u) * 0x54);
62 if (cmd->command_number != seq) 64 if (cmd->command_number != seq)
63 return nullptr; 65 return nullptr;
64 return cmd; 66 return cmd;
65} 67}
66 68
67void fix_verified_crc(int seq, uint32_t crc) { 69void fix_verified_crc(int seq, uint32_t crc) {
68 if (!g_input) 70 if (!g_input || seq < 0)
69 return; 71 return;
70 uintptr_t verified = *(uintptr_t *)(g_input + CMDOFF_M_PVERIFIED); 72 uintptr_t verified = *(uintptr_t *)(g_input + CMDOFF_M_PVERIFIED);
71 if (verified) 73 if (verified)
72 *(uint32_t *)(verified + (size_t)(seq % 90) * VERIFIED_STRIDE + 74 *(uint32_t *)(verified + (size_t)((unsigned)seq % 90u) * VERIFIED_STRIDE +
73 VERIFIED_CRC_OFF) = crc; 75 VERIFIED_CRC_OFF) = crc;
74} 76}
75 77
@@ -92,6 +94,11 @@ static InputExtraMouseFn oInputExtraMouseSample = nullptr;
92// commands. While the GUI is open it must not touch the view; movement still 94// commands. While the GUI is open it must not touch the view; movement still
93// comes through CreateMove. 95// comes through CreateMove.
94static void hkdInputExtraMouseSample(void *self, float frametime, bool active) { 96static void hkdInputExtraMouseSample(void *self, float frametime, bool active) {
97 if (!init_live()) {
98 if (oInputExtraMouseSample)
99 oInputExtraMouseSample(self, frametime, active);
100 return;
101 }
95 if (gui_is_open()) 102 if (gui_is_open())
96 return; 103 return;
97 if (oInputExtraMouseSample) 104 if (oInputExtraMouseSample)
@@ -109,6 +116,11 @@ void input_clear_mouse_states() {
109 116
110static void hkdInputCreateMove(void *self, int seq, float frametime, 117static void hkdInputCreateMove(void *self, int seq, float frametime,
111 bool active) { 118 bool active) {
119 if (!init_live()) {
120 if (oInputCreateMove)
121 oInputCreateMove(self, seq, frametime, active);
122 return;
123 }
112 // Mouse deltas are accumulated between ticks. With the custom GUI open we 124 // Mouse deltas are accumulated between ticks. With the custom GUI open we
113 // must discard them before CInput::CreateMove applies mouse look, otherwise 125 // must discard them before CInput::CreateMove applies mouse look, otherwise
114 // the view moves for one frame and snaps back when do_move freezes it. 126 // the view moves for one frame and snaps back when do_move freezes it.
@@ -151,8 +163,11 @@ static bool install_input_hook() {
151 if (!vt) 163 if (!vt)
152 return false; 164 return false;
153 constexpr size_t N = 64; 165 constexpr size_t N = 64;
154 void **nvt = new void *[N]; 166 void **nvt = vtable_clone((void *)g_input, N);
155 memcpy(nvt, vt, N * sizeof(void *)); 167 if (!nvt) {
168 logf("CInput vtable clone failed");
169 return false;
170 }
156 oInputCreateMove = (InputCreateMoveFn)vt[3]; 171 oInputCreateMove = (InputCreateMoveFn)vt[3];
157 nvt[3] = (void *)&hkdInputCreateMove; 172 nvt[3] = (void *)&hkdInputCreateMove;
158 if (N > 4) { 173 if (N > 4) {
@@ -173,7 +188,7 @@ static bool install_input_hook() {
173bool find_input_global() { 188bool find_input_global() {
174 void **vt = VTABLE(g_iface.client); 189 void **vt = VTABLE(g_iface.client);
175 uintptr_t cm = (uintptr_t)vt[21]; // CreateMove slot 190 uintptr_t cm = (uintptr_t)vt[21]; // CreateMove slot
176 if (!g_client.contains(cm)) { 191 if (!mod_client().contains(cm)) {
177 logf("CreateMove %p outside client module", (void *)cm); 192 logf("CreateMove %p outside client module", (void *)cm);
178 return false; 193 return false;
179 } 194 }
@@ -189,7 +204,7 @@ bool find_input_global() {
189 break; 204 break;
190 } 205 }
191 } 206 }
192 if (!anchor || !g_client.contains(anchor)) { 207 if (!anchor || !mod_client().contains(anchor)) {
193 logf("PIC anchor not found in CreateMove"); 208 logf("PIC anchor not found in CreateMove");
194 return false; 209 return false;
195 } 210 }
@@ -214,16 +229,16 @@ bool find_input_global() {
214 uint32_t disp; 229 uint32_t disp;
215 memcpy(&disp, b + i + 2, 4); 230 memcpy(&disp, b + i + 2, 4);
216 uintptr_t var = anchor + disp; 231 uintptr_t var = anchor + disp;
217 if (!g_client.contains(var)) 232 if (!mod_client().contains(var))
218 continue; 233 continue;
219 uintptr_t input = *(uintptr_t *)var; 234 uintptr_t input = *(uintptr_t *)var;
220 if (!input || !g_client.contains(input)) 235 if (!input || !mod_client().contains(input))
221 continue; 236 continue;
222 uintptr_t vtbl = *(uintptr_t *)input; 237 uintptr_t vtbl = *(uintptr_t *)input;
223 if (!vtbl || !g_client.contains(vtbl)) 238 if (!vtbl || !mod_client().contains(vtbl))
224 continue; 239 continue;
225 uintptr_t f3 = *(uintptr_t *)(vtbl + 3 * sizeof(void *)); 240 uintptr_t f3 = *(uintptr_t *)(vtbl + 3 * sizeof(void *));
226 if (!g_client.contains(f3)) 241 if (!mod_client().contains(f3))
227 continue; 242 continue;
228 logd("CInput* = %#lx (ptr var %#lx, vfunc[3] %#lx)", (unsigned long)input, 243 logd("CInput* = %#lx (ptr var %#lx, vfunc[3] %#lx)", (unsigned long)input,
229 (unsigned long)var, (unsigned long)f3); 244 (unsigned long)var, (unsigned long)f3);
diff --git a/src/esp.cpp b/src/esp.cpp
index 72110cc..81ec81c 100644
--- a/src/esp.cpp
+++ b/src/esp.cpp
@@ -7,12 +7,6 @@
7#include <cstdlib> 7#include <cstdlib>
8#include <ctime> 8#include <ctime>
9 9
10// ISurface slots (binary = SDK header + 4, disassembly-confirmed)
11enum SurfSlot {
12 SURF_DrawSetColor = 10, SURF_FilledRect = 12, SURF_OutlinedRect = 14,
13 SURF_DrawLine = 15, SURF_GetScreenSize = 38,
14};
15
16enum { LIFE_ALIVE = 0 }; 10enum { LIFE_ALIVE = 0 };
17 11
18struct EspTarget { 12struct EspTarget {
@@ -40,6 +34,36 @@ static int g_ntargets = 0;
40static int g_scr_w = 0, g_scr_h = 0; 34static int g_scr_w = 0, g_scr_h = 0;
41static bool g_esp_logged = false; 35static bool g_esp_logged = false;
42 36
37// Listen-server sanity latch. Only ever set once per map (esp_reset_session),
38// and only after both model indices are valid: latching on the transient
39// cli_model == 0 at map load disabled server health for the whole process.
40static bool g_srv_checked = false;
41static bool g_srv_trusted = false;
42
43// Per-map caches. The ESP resolves a model name for every entity on every scan
44// and the chams hook asks for a relation on every model draw; both are virtual
45// call chains into the engine plus server datamap reads, so memoise them.
46static constexpr int MODEL_CACHE = 16384;
47static const char *g_model_name_cache[MODEL_CACHE];
48static unsigned char g_model_state_cache[MODEL_CACHE]; // 0 unknown,1 ok,2 fail
49
50// Direct-mapped entity pointer -> index map, filled by esp_collect. The
51// triggerbot traces a ray and gets a pointer back, and used to find its index
52// by walking up to 4096 entity-list slots on every fire tick.
53static constexpr int ENTPTR_CACHE = 4096;
54static void *g_entptr_key[ENTPTR_CACHE];
55static int g_entptr_idx[ENTPTR_CACHE];
56
57void esp_reset_session() {
58 g_srv_checked = false;
59 g_srv_trusted = false;
60 g_esp_logged = false;
61 memset(g_model_name_cache, 0, sizeof(g_model_name_cache));
62 memset(g_model_state_cache, 0, sizeof(g_model_state_cache));
63 memset(g_entptr_key, 0, sizeof(g_entptr_key));
64 memset(g_entptr_idx, 0, sizeof(g_entptr_idx));
65}
66
43// Speed graph: 100 samples at 0.1 s = the last 10 seconds. 67// Speed graph: 100 samples at 0.1 s = the last 10 seconds.
44static constexpr int SPEED_SAMPLES = 100; 68static constexpr int SPEED_SAMPLES = 100;
45static float g_speed_samples[SPEED_SAMPLES] = {}; 69static float g_speed_samples[SPEED_SAMPLES] = {};
@@ -52,6 +76,21 @@ static double esp_now() {
52 return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; 76 return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
53} 77}
54 78
79// Refresh the cached screen size every frame. It used to be read once, so a
80// resolution or window change left every projection, screen-edge clamp and
81// centering calculation stale for the rest of the session.
82static bool esp_update_screen() {
83 if (!g_surface)
84 return false;
85 int w = 0, h = 0;
86 overlay_screen_size(w, h);
87 if (w > 0)
88 g_scr_w = w;
89 if (h > 0)
90 g_scr_h = h;
91 return g_scr_w > 0 && g_scr_h > 0;
92}
93
55// Local perspective projection used when the engine's debug-overlay 94// Local perspective projection used when the engine's debug-overlay
56// projection refuses a point that is actually in front of the camera. 95// projection refuses a point that is actually in front of the camera.
57static bool project_manual(const Vector &w, float &sx, float &sy) { 96static bool project_manual(const Vector &w, float &sx, float &sy) {
@@ -59,11 +98,16 @@ static bool project_manual(const Vector &w, float &sx, float &sy) {
59 return false; 98 return false;
60 float pitch = g_cam_view.x * PI_F / 180.0f; 99 float pitch = g_cam_view.x * PI_F / 180.0f;
61 float yaw = g_cam_view.y * PI_F / 180.0f; 100 float yaw = g_cam_view.y * PI_F / 180.0f;
101 float roll = g_cam_view.z * PI_F / 180.0f;
62 float sp = sinf(pitch), cp = cosf(pitch); 102 float sp = sinf(pitch), cp = cosf(pitch);
63 float syy = sinf(yaw), cy = cosf(yaw); 103 float syy = sinf(yaw), cy = cosf(yaw);
104 float sr = sinf(roll), cr = cosf(roll);
64 float fwd[3] = {cp * cy, cp * syy, -sp}; 105 float fwd[3] = {cp * cy, cp * syy, -sp};
65 float right[3] = {syy, -cy, 0}; 106 // Source's AngleVectors with roll; the old right/up vectors assumed roll 0,
66 float up[3] = {sp * cy, sp * syy, cp}; 107 // so view punch made the fallback projection rotate against the world.
108 float right[3] = {-sr * sp * cy + cr * syy, -sr * sp * syy - cr * cy,
109 -sr * cp};
110 float up[3] = {cr * sp * cy + sr * syy, cr * sp * syy - sr * cy, cr * cp};
67 float dx = w.x - g_cam_eye.x, dy = w.y - g_cam_eye.y, dz = w.z - g_cam_eye.z; 111 float dx = w.x - g_cam_eye.x, dy = w.y - g_cam_eye.y, dz = w.z - g_cam_eye.z;
68 float z = dx * fwd[0] + dy * fwd[1] + dz * fwd[2]; 112 float z = dx * fwd[0] + dy * fwd[1] + dz * fwd[2];
69 if (z < 1.0f) 113 if (z < 1.0f)
@@ -71,7 +115,10 @@ static bool project_manual(const Vector &w, float &sx, float &sy) {
71 float x = dx * right[0] + dy * right[1] + dz * right[2]; 115 float x = dx * right[0] + dy * right[1] + dz * right[2];
72 float y = dx * up[0] + dy * up[1] + dz * up[2]; 116 float y = dx * up[0] + dy * up[1] + dz * up[2];
73 float fov = g_fov_camera > 10 ? (float)g_fov_camera : 75.0f; 117 float fov = g_fov_camera > 10 ? (float)g_fov_camera : 75.0f;
74 float tanfov = tanf(fov * 0.5f * PI_F / 180.0f); 118 // Source defines the FOV for a 4:3 reference and widens it on widescreen;
119 // using a raw tan(fov/2) made the fallback ~33% too narrow at 16:9.
120 float aspect = (float)g_scr_w / (float)g_scr_h;
121 float tanfov = tanf(fov * 0.5f * PI_F / 180.0f) * (aspect / (4.0f / 3.0f));
75 sx = (float)g_scr_w * 0.5f + (x / z) * ((float)g_scr_w * 0.5f / tanfov); 122 sx = (float)g_scr_w * 0.5f + (x / z) * ((float)g_scr_w * 0.5f / tanfov);
76 sy = (float)g_scr_h * 0.5f - (y / z) * ((float)g_scr_w * 0.5f / tanfov); 123 sy = (float)g_scr_h * 0.5f - (y / z) * ((float)g_scr_w * 0.5f / tanfov);
77 return std::isfinite(sx) && std::isfinite(sy) && fabsf(sx) < 1e7f && 124 return std::isfinite(sx) && std::isfinite(sy) && fabsf(sx) < 1e7f &&
@@ -137,16 +184,96 @@ static const char *entity_class_name(void *net, void **out_cc) {
137 return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr; 184 return cc ? *(const char **)((uintptr_t)cc + 8) : nullptr;
138} 185}
139 186
187// IClientNetworkable slot 8 = IsDormant (SDK order: GetClientClass 2, IsDormant
188// 8, entindex 9). The shipped binary is not guaranteed to match the 2013 SDK,
189// so the slot is validated once against the local player, which can never be
190// dormant: a wrong slot disables the check instead of misreporting entities.
191// Dormant entities keep their last networked origin, so drawing or aiming at
192// them produces ghosts at stale positions.
193static int g_dormant_state = 0; // 0 untested, 1 enabled, -1 disabled
194
195static void dormant_selftest() {
196 // Leave the state untested (0) until a local player exists: the first call
197 // can come from the main menu, and latching "disabled" there would turn the
198 // check off for the whole session.
199 if (!g_iface.entitylist)
200 return;
201 int local = engine_get_local_player();
202 if (local <= 0)
203 return;
204 void *net = entity_networkable(local);
205 if (!net)
206 return;
207 void **vt = *(void ***)net;
208 if (!vt || !mod_client().contains((uintptr_t)vt[8])) {
209 g_dormant_state = -1;
210 logf("dormant check disabled: slot 8 outside client module");
211 return;
212 }
213 using DormantFn = unsigned char (*)(void *);
214 if (vfunc<DormantFn>(net, 8)(net) != 0) {
215 g_dormant_state = -1;
216 logf("dormant check disabled: local player reported dormant");
217 return;
218 }
219 g_dormant_state = 1;
220 logd("dormant check enabled");
221}
222
223static bool entity_is_dormant(void *net) {
224 if (!net)
225 return false;
226 if (g_dormant_state == 0)
227 dormant_selftest();
228 if (g_dormant_state != 1)
229 return false;
230 using DormantFn = unsigned char (*)(void *);
231 return vfunc<DormantFn>(net, 8)(net) != 0;
232}
233
234// Entity pointer -> index, filled by the entity scans. The triggerbot gets a
235// pointer back from the trace and needs the index; the alternative is walking
236// up to 4096 entity-list slots on every fire tick.
237static int entptr_lookup(void *ent) {
238 if (!ent)
239 return -1;
240 unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1));
241 return g_entptr_key[h] == ent ? g_entptr_idx[h] : -1;
242}
243
244static void entptr_store(void *ent, int idx) {
245 if (!ent || idx <= 0)
246 return;
247 unsigned h = (unsigned)(((uintptr_t)ent >> 4) & (ENTPTR_CACHE - 1));
248 g_entptr_key[h] = ent;
249 g_entptr_idx[h] = idx;
250}
251
140// m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName. 252// m_nModelIndex -> IVModelInfoClient(006)::GetModel/GetModelName.
141// Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4. 253// Itanium ABI: two dtor slots, so GetModel=2, GetModelName=4.
142static const char *model_name_from_index(int modelindex) { 254static const char *model_name_from_index(int modelindex) {
143 if (!g_iface.modelinfo || modelindex <= 0) 255 if (!g_iface.modelinfo || modelindex <= 0)
144 return nullptr; 256 return nullptr;
257 if (modelindex < MODEL_CACHE) {
258 if (g_model_state_cache[modelindex] == 1)
259 return g_model_name_cache[modelindex];
260 if (g_model_state_cache[modelindex] == 2)
261 return nullptr;
262 }
145 const void *model = 263 const void *model =
146 vfunc<GetModelFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex); 264 vfunc<GetModelFn>(g_iface.modelinfo, 2)(g_iface.modelinfo, modelindex);
147 if (!model) 265 if (!model) {
266 if (modelindex < MODEL_CACHE)
267 g_model_state_cache[modelindex] = 2;
148 return nullptr; 268 return nullptr;
149 return vfunc<GetModelNameFn>(g_iface.modelinfo, 4)(g_iface.modelinfo, model); 269 }
270 const char *name =
271 vfunc<GetModelNameFn>(g_iface.modelinfo, 4)(g_iface.modelinfo, model);
272 if (modelindex < MODEL_CACHE) {
273 g_model_name_cache[modelindex] = name;
274 g_model_state_cache[modelindex] = name ? 1 : 2;
275 }
276 return name;
150} 277}
151 278
152static const char *entity_model_name(void *ent) { 279static const char *entity_model_name(void *ent) {
@@ -155,6 +282,19 @@ static const char *entity_model_name(void *ent) {
155 return model_name_from_index(modelindex); 282 return model_name_from_index(modelindex);
156} 283}
157 284
285// Live origin for a cached entity index. Item/projectile discovery is throttled
286// to 10 Hz (it walks the whole entity list), but the markers must follow the
287// world at render rate; refreshing one netvar read per cached target is cheap.
288static bool entity_origin(void *ent, Vector &out) {
289 if (!ent || g_net.origin < 0)
290 return false;
291 uintptr_t e = (uintptr_t)ent;
292 out.x = *(float *)(e + g_net.origin);
293 out.y = *(float *)(e + g_net.origin + 4);
294 out.z = *(float *)(e + g_net.origin + 8);
295 return true;
296}
297
158// HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all 298// HL2 SP: generic NPCs (combine, metrocop, zombie, citizen, antlion, ...) all
159// use the single client class "CAI_BaseNPC" — only special NPCs register their 299// use the single client class "CAI_BaseNPC" — only special NPCs register their
160// own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...). 300// own class names (CNPC_Manhack, CNPC_Strider, CNPC_CombineGunship, ...).
@@ -192,20 +332,35 @@ bool esp_is_viewmodel_index(int idx) {
192static bool npc_is_friendly(int idx, const char *model, int local_idx, 332static bool npc_is_friendly(int idx, const char *model, int local_idx,
193 bool &neutral); 333 bool &neutral);
194int esp_npc_relation(int idx) { 334int esp_npc_relation(int idx) {
195 if (!esp_is_npc_index(idx)) 335 // Called from the chams hook up to three times per entity per frame (visible,
196 return -1; 336 // through-wall, wireframe pass) and the work behind it is a chain of server
197 int local = engine_get_local_player(); 337 // edict/datamap reads. A short TTL cache keeps that out of the render path.
198 if (local <= 0) 338 struct RelCache {
199 return 0; 339 int idx;
200 void *ent = entitylist_get(idx); 340 double at;
201 const char *model = ent ? entity_model_name(ent) : nullptr; 341 int val;
202 bool neutral = false; 342 };
203 bool friendly = npc_is_friendly(idx, model, local, neutral); 343 static RelCache cache[256];
204 if (neutral) 344 double now = esp_now();
205 return 1; 345 RelCache &c = cache[(unsigned)idx & 255];
206 if (friendly) 346 if (c.idx == idx && now - c.at < 0.05)
207 return 2; 347 return c.val;
208 return 0; 348
349 int val = -1;
350 if (esp_is_npc_index(idx)) {
351 int local = engine_get_local_player();
352 if (local <= 0) {
353 val = 0;
354 } else {
355 void *ent = entitylist_get(idx);
356 const char *model = ent ? entity_model_name(ent) : nullptr;
357 bool neutral = false;
358 bool friendly = npc_is_friendly(idx, model, local, neutral);
359 val = neutral ? 1 : friendly ? 2 : 0;
360 }
361 }
362 c = {idx, now, val};
363 return val;
209} 364}
210 365
211static bool model_is_friendly(const char *m) { 366static bool model_is_friendly(const char *m) {
@@ -237,8 +392,8 @@ static bool model_is_floor_turret(const char *m) {
237 return m && strstr(m, "floor_turret"); 392 return m && strstr(m, "floor_turret");
238} 393}
239 394
240static bool npc_is_friendly(int idx, const char *model, int local_idx, 395static bool npc_is_friendly_uncached(int idx, const char *model, int local_idx,
241 bool &neutral) { 396 bool &neutral) {
242 int rel = server_npc_relation(idx, local_idx); 397 int rel = server_npc_relation(idx, local_idx);
243 int state = server_npc_state(idx); 398 int state = server_npc_state(idx);
244 int turret = -1; 399 int turret = -1;
@@ -302,6 +457,28 @@ static bool npc_is_friendly(int idx, const char *model, int local_idx,
302 return friendly; 457 return friendly;
303} 458}
304 459
460// Friendly/neutral state changes slowly (hacked turrets, bugbait), while the
461// entity scan and the chams hook ask for it constantly. Cache it briefly so the
462// server lookups happen once per NPC per window instead of per call.
463static bool npc_is_friendly(int idx, const char *model, int local_idx,
464 bool &neutral) {
465 struct Cache {
466 int idx;
467 double at;
468 bool friendly;
469 bool neutral;
470 };
471 static Cache cache[256];
472 double now = esp_now();
473 Cache &c = cache[(unsigned)idx & 255];
474 if (c.idx == idx && now - c.at < 0.10) {
475 neutral = c.neutral;
476 return c.friendly;
477 }
478 bool f = npc_is_friendly_uncached(idx, model, local_idx, neutral);
479 c = {idx, now, f, neutral};
480 return f;
481}
305 482
306enum NpcDamageClass { 483enum NpcDamageClass {
307 NPC_DMG_NORMAL = 0, 484 NPC_DMG_NORMAL = 0,
@@ -316,7 +493,7 @@ static bool model_has(const char *model, const char *needle) {
316 493
317// Server m_takedamage first (authoritative immunity), then the HL2 damage 494// Server m_takedamage first (authoritative immunity), then the HL2 damage
318// rules for units whose OnTakeDamage filters specific damage types. 495// rules for units whose OnTakeDamage filters specific damage types.
319static int npc_damage_class(int idx, const char *model) { 496static int npc_damage_class_uncached(int idx, const char *model) {
320 int td = server_npc_takedamage(idx); 497 int td = server_npc_takedamage(idx);
321 if (td == 0 || td == 1) // DAMAGE_NO / DAMAGE_EVENTS_ONLY 498 if (td == 0 || td == 1) // DAMAGE_NO / DAMAGE_EVENTS_ONLY
322 return NPC_DMG_IMMUNE; 499 return NPC_DMG_IMMUNE;
@@ -330,6 +507,24 @@ static int npc_damage_class(int idx, const char *model) {
330 return NPC_DMG_NORMAL; 507 return NPC_DMG_NORMAL;
331} 508}
332 509
510// Damage class depends on m_takedamage (a server read) plus model string
511// matching. It changes even more rarely than the relation, so cache it.
512static int npc_damage_class(int idx, const char *model) {
513 struct Cache {
514 int idx;
515 double at;
516 int val;
517 };
518 static Cache cache[256];
519 double now = esp_now();
520 Cache &c = cache[(unsigned)idx & 255];
521 if (c.idx == idx && now - c.at < 0.25)
522 return c.val;
523 int v = npc_damage_class_uncached(idx, model);
524 c = {idx, now, v};
525 return v;
526}
527
333static bool active_weapon_contains(void *local_ent, const char *needle); 528static bool active_weapon_contains(void *local_ent, const char *needle);
334 529
335// Armoured units need blast damage; the RPG is the only stock weapon that 530// Armoured units need blast damage; the RPG is the only stock weapon that
@@ -430,15 +625,14 @@ static void esp_collect(int local_idx) {
430 if (hi < 0 || hi > 4096) hi = 4096; 625 if (hi < 0 || hi > 4096) hi = 4096;
431 int npc_alive = 0, dbg_lines = 0; 626 int npc_alive = 0, dbg_lines = 0;
432 bool srv = resolve_server_netvars(); 627 bool srv = resolve_server_netvars();
433 static bool srv_checked = false;
434 static bool srv_trusted = false;
435 628
436 for (int idx = 1; idx <= hi && g_ntargets < 128; idx++) { 629 for (int idx = 1; idx <= hi && g_ntargets < 128; idx++) {
437 if (idx == local_idx) continue; 630 if (idx == local_idx) continue;
438 void *ent = entitylist_get(idx); 631 void *ent = entitylist_get(idx);
439 if (!ent) continue; 632 if (!ent) continue;
633 entptr_store(ent, idx);
440 void *net = entity_networkable(idx); 634 void *net = entity_networkable(idx);
441 if (!net) continue; 635 if (!net || entity_is_dormant(net)) continue;
442 const char *cls = entity_class_name(net, nullptr); 636 const char *cls = entity_class_name(net, nullptr);
443 if (!cls) continue; 637 if (!cls) continue;
444 638
@@ -456,19 +650,24 @@ static void esp_collect(int local_idx) {
456 int slife = -1, smax = -1; 650 int slife = -1, smax = -1;
457 void *sbase = nullptr; 651 void *sbase = nullptr;
458 int shp = server_entity_health(idx, &smax, &slife, &sbase); 652 int shp = server_entity_health(idx, &smax, &slife, &sbase);
459 if (!srv_checked && sbase) { 653 if (!g_srv_checked && sbase) {
460 srv_checked = true;
461 int cli_mi = 654 int cli_mi =
462 g_net.modelindex >= 0 ? *(int *)(e + g_net.modelindex) : -1; 655 g_net.modelindex >= 0 ? *(int *)(e + g_net.modelindex) : -1;
463 int srv_mi = server_model_index(sbase); 656 int srv_mi = server_model_index(sbase);
464 srv_trusted = (cli_mi == srv_mi); 657 // Wait for both indices: the client index is often 0 for a tick right
465 logd("ESP server check: idx=%d base=%p cli_model=%d srv_model=%d " 658 // after a map load, which used to latch "untrusted" permanently.
466 "hp=%d maxhp=%d life=%d trusted=%d", 659 if (cli_mi > 0 && srv_mi > 0) {
467 idx, sbase, cli_mi, srv_mi, shp, smax, slife, (int)srv_trusted); 660 g_srv_checked = true;
468 if (!srv_trusted) 661 g_srv_trusted = (cli_mi == srv_mi);
469 logf("ESP: server entity mismatch, ignoring server values"); 662 logd("ESP server check: idx=%d base=%p cli_model=%d srv_model=%d "
663 "hp=%d maxhp=%d life=%d trusted=%d",
664 idx, sbase, cli_mi, srv_mi, shp, smax, slife,
665 (int)g_srv_trusted);
666 if (!g_srv_trusted)
667 logf("ESP: server entity mismatch, ignoring server values");
668 }
470 } 669 }
471 if (srv_trusted) { 670 if (g_srv_trusted) {
472 if (shp >= 0) hp = shp; 671 if (shp >= 0) hp = shp;
473 if (smax > 0) maxhp = smax; 672 if (smax > 0) maxhp = smax;
474 if (slife >= 0) life = slife; 673 if (slife >= 0) life = slife;
@@ -576,16 +775,16 @@ static void esp_collect(int local_idx) {
576} 775}
577 776
578static void surf_rect(int filled, int x0, int y0, int x1, int y1) { 777static void surf_rect(int filled, int x0, int y0, int x1, int y1) {
579 using Fn = void (*)(void *, int, int, int, int); 778 if (filled)
580 vfunc<Fn>(g_surface, filled ? SURF_FilledRect : SURF_OutlinedRect)(g_surface, x0, y0, x1, y1); 779 overlay_fill(x0, y0, x1, y1);
780 else
781 overlay_outline_rect(x0, y0, x1, y1);
581} 782}
582static void surf_color(int r, int g, int b, int a) { 783static void surf_color(int r, int g, int b, int a) {
583 using Fn = void (*)(void *, int, int, int, int); 784 overlay_color(r, g, b, a);
584 vfunc<Fn>(g_surface, SURF_DrawSetColor)(g_surface, r, g, b, a);
585} 785}
586static void surf_line(int x0, int y0, int x1, int y1) { 786static void surf_line(int x0, int y0, int x1, int y1) {
587 using Fn = void (*)(void *, int, int, int, int); 787 overlay_line(x0, y0, x1, y1);
588 vfunc<Fn>(g_surface, SURF_DrawLine)(g_surface, x0, y0, x1, y1);
589} 788}
590 789
591// Screen projection of the aimbot FOV cone boundary, using the engine's own 790// Screen projection of the aimbot FOV cone boundary, using the engine's own
@@ -599,8 +798,14 @@ static QAngle engine_view_angles() {
599 return v; 798 return v;
600} 799}
601 800
602static void draw_fov_circle() { 801void esp_draw_fov_circle() {
603 if (!aimbot_fov_circle()) 802 if (!g_surface || !aimbot_fov_circle())
803 return;
804 // No local player means no game world (main menu); the cached camera is stale
805 // and the projected circle would land at a meaningless spot.
806 if (engine_get_local_player() <= 0)
807 return;
808 if (!esp_update_screen())
604 return; 809 return;
605 float fov = (float)aimbot_fov(); 810 float fov = (float)aimbot_fov();
606 if (fov < 1.0f || fov >= 89.0f) // beyond this the "circle" is behind the eye 811 if (fov < 1.0f || fov >= 89.0f) // beyond this the "circle" is behind the eye
@@ -812,7 +1017,9 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
812 out_idx = -1; 1017 out_idx = -1;
813 out_point = {}; 1018 out_point = {};
814 out_fov = 0.0f; 1019 out_fov = 0.0f;
815 if (!g_iface.entitylist || g_net.origin < 0 || g_net.maxs < 0) 1020 // maxs is optional: the per-target code falls back to 72 units when the
1021 // netvar is missing, so it must not disable the whole aimbot here.
1022 if (!g_iface.entitylist || g_net.origin < 0)
816 return false; 1023 return false;
817 int local = engine_get_local_player(); 1024 int local = engine_get_local_player();
818 if (local <= 0) 1025 if (local <= 0)
@@ -830,7 +1037,8 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
830 bool rpg = active_weapon_contains(local_ent, "rocket_launcher") || 1037 bool rpg = active_weapon_contains(local_ent, "rocket_launcher") ||
831 active_weapon_contains(local_ent, "rpg"); 1038 active_weapon_contains(local_ent, "rpg");
832 1039
833 auto try_target = [&](int idx, bool enforce_fov, Vector &pt, 1040 // fov_limit <= 0 means "no angular limit" (kept for unlimited acquisition).
1041 auto try_target = [&](int idx, float fov_limit, Vector &pt,
834 float &fov) -> bool { 1042 float &fov) -> bool {
835 if (idx <= 0 || idx == local) 1043 if (idx <= 0 || idx == local)
836 return false; 1044 return false;
@@ -838,7 +1046,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
838 if (!ent) 1046 if (!ent)
839 return false; 1047 return false;
840 void *net = entity_networkable(idx); 1048 void *net = entity_networkable(idx);
841 if (!net) 1049 if (!net || entity_is_dormant(net))
842 return false; 1050 return false;
843 const char *cls = entity_class_name(net, nullptr); 1051 const char *cls = entity_class_name(net, nullptr);
844 if (!cls || !is_npc_class(cls)) 1052 if (!cls || !is_npc_class(cls))
@@ -863,7 +1071,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
863 Vector best_pt{}; 1071 Vector best_pt{};
864 for (int i = 0; i < npoints; i++) { 1072 for (int i = 0; i < npoints; i++) {
865 float f = getfov(view, angle_to(g_cam_eye, pts[i])); 1073 float f = getfov(view, angle_to(g_cam_eye, pts[i]));
866 if (enforce_fov && f > max_fov) 1074 if (fov_limit > 0.0f && f > fov_limit)
867 continue; 1075 continue;
868 if (f >= best) 1076 if (f >= best)
869 continue; 1077 continue;
@@ -880,11 +1088,13 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
880 }; 1088 };
881 1089
882 // Keep the locked target while it is alive and has a visible selected point. 1090 // Keep the locked target while it is alive and has a visible selected point.
883 // No FOV limit on a locked target; only acquisition is FOV-limited. 1091 // The lock uses a wider cone than acquisition (hysteresis), so a target that
1092 // drifts slightly off the crosshair is not dropped, but one that swings
1093 // behind the player is: an unbounded lock let silent aim fire backwards.
884 if (lock_idx && *lock_idx >= 0) { 1094 if (lock_idx && *lock_idx >= 0) {
885 Vector pt; 1095 Vector pt;
886 float f; 1096 float f;
887 if (try_target(*lock_idx, false, pt, f)) { 1097 if (try_target(*lock_idx, max_fov * 1.5f, pt, f)) {
888 out_idx = *lock_idx; 1098 out_idx = *lock_idx;
889 out_point = pt; 1099 out_point = pt;
890 out_fov = f; 1100 out_fov = f;
@@ -920,7 +1130,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
920 if (!ent) 1130 if (!ent)
921 continue; 1131 continue;
922 void *net = entity_networkable(idx); 1132 void *net = entity_networkable(idx);
923 if (!net) 1133 if (!net || entity_is_dormant(net))
924 continue; 1134 continue;
925 const char *cls = entity_class_name(net, nullptr); 1135 const char *cls = entity_class_name(net, nullptr);
926 if (!cls || !is_npc_class(cls)) 1136 if (!cls || !is_npc_class(cls))
@@ -949,7 +1159,17 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
949 Vector approx{origin.x, origin.y, 1159 Vector approx{origin.x, origin.y,
950 origin.z + maxz * ((bone_mask & (1u << 1)) ? 0.9f : 0.55f)}; 1160 origin.z + maxz * ((bone_mask & (1u << 1)) ? 0.9f : 0.55f)};
951 float fov = getfov(view, angle_to(g_cam_eye, approx)); 1161 float fov = getfov(view, angle_to(g_cam_eye, approx));
952 if (fov > max_fov) 1162 // The prefilter samples a single point, but the selected hitbox can be a
1163 // whole body-width away at close range. Reject only when the target's
1164 // angular radius cannot reach the cone; try_target still enforces the
1165 // exact per-hitbox FOV.
1166 float ddx = approx.x - g_cam_eye.x;
1167 float ddy = approx.y - g_cam_eye.y;
1168 float ddz = approx.z - g_cam_eye.z;
1169 float dist = sqrtf(ddx * ddx + ddy * ddy + ddz * ddz);
1170 float margin =
1171 dist > 1.0f ? atan2f(maxz, dist) * 180.0f / PI_F : 90.0f;
1172 if (fov - margin > max_fov)
953 continue; 1173 continue;
954 float key = (rpg && armored) ? fov - 1000.0f : fov; 1174 float key = (rpg && armored) ? fov - 1000.0f : fov;
955 1175
@@ -977,7 +1197,7 @@ static bool select_aim_target(const QAngle &view, unsigned bone_mask,
977 for (int ci = 0; ci < ncands; ci++) { 1197 for (int ci = 0; ci < ncands; ci++) {
978 Vector pt; 1198 Vector pt;
979 float f; 1199 float f;
980 if (!try_target(cands[ci].idx, true, pt, f)) 1200 if (!try_target(cands[ci].idx, max_fov, pt, f))
981 continue; 1201 continue;
982 out_idx = cands[ci].idx; 1202 out_idx = cands[ci].idx;
983 out_point = pt; 1203 out_point = pt;
@@ -1043,9 +1263,28 @@ static bool projectile_lead_point(int target_idx, const Vector &point,
1043 return false; 1263 return false;
1044 } 1264 }
1045 void *target = entitylist_get(target_idx); 1265 void *target = entitylist_get(target_idx);
1046 if (!target || g_net.velocity < 0) 1266 if (!target)
1267 return false;
1268 // NPCs do not network C_BaseEntity::m_vecVelocity (only the player does), so
1269 // the client field is stale for exactly the targets the aimbot uses. Read
1270 // the listen-server entity when available and fall back to the client value.
1271 float v[3] = {0.0f, 0.0f, 0.0f};
1272 Vector srv_vel;
1273 bool have_vel = false;
1274 if (server_entity_velocity(target_idx, &srv_vel)) {
1275 v[0] = srv_vel.x;
1276 v[1] = srv_vel.y;
1277 v[2] = srv_vel.z;
1278 have_vel = true;
1279 } else if (g_net.velocity >= 0) {
1280 const float *cv = (const float *)((uintptr_t)target + g_net.velocity);
1281 v[0] = cv[0];
1282 v[1] = cv[1];
1283 v[2] = cv[2];
1284 have_vel = true;
1285 }
1286 if (!have_vel)
1047 return false; 1287 return false;
1048 const float *v = (const float *)((uintptr_t)target + g_net.velocity);
1049 float dx = point.x - g_cam_eye.x; 1288 float dx = point.x - g_cam_eye.x;
1050 float dy = point.y - g_cam_eye.y; 1289 float dy = point.y - g_cam_eye.y;
1051 float dz = point.z - g_cam_eye.z; 1290 float dz = point.z - g_cam_eye.z;
@@ -1058,6 +1297,51 @@ static bool projectile_lead_point(int target_idx, const Vector &point,
1058 return true; 1297 return true;
1059} 1298}
1060 1299
1300// A led point is a prediction, not a verified aim point: acquisition checked
1301// the un-led position, so the shift has to pass the same FOV and visibility
1302// tests. On failure the caller keeps the original point instead of firing into
1303// geometry or outside the configured cone.
1304static bool lead_point_ok(int target_idx, const Vector &lead,
1305 const QAngle &view, float max_fov) {
1306 if (getfov(view, angle_to(g_cam_eye, lead)) > max_fov)
1307 return false;
1308 if (!aimbot_vischeck_enabled())
1309 return true;
1310 void *local_ent = entitylist_get(engine_get_local_player());
1311 void *target_ent = entitylist_get(target_idx);
1312 if (!local_ent || !target_ent)
1313 return false;
1314 return is_visible(g_cam_eye, lead, local_ent, target_ent);
1315}
1316
1317// Semi-auto weapons (pistol, .357, crossbow, shotgun, RPG, gravity gun) only
1318// fire on the IN_ATTACK press edge, so a synthetic button held every tick fires
1319// exactly once per buy/equip. Alternate press/release for those; automatic
1320// weapons keep the button held.
1321static bool weapon_is_semi_auto(void *local_ent) {
1322 static const char *names[] = {"pistol", "357",
1323 "crossbow", "shotgun",
1324 "rocket_launcher", "physcannon",
1325 "crowbar", nullptr};
1326 for (int i = 0; names[i]; i++)
1327 if (active_weapon_contains(local_ent, names[i]))
1328 return true;
1329 return false;
1330}
1331
1332static bool fire_edge_ok(CUserCmd *cmd) {
1333 void *local_ent = entitylist_get(engine_get_local_player());
1334 if (!local_ent || !weapon_is_semi_auto(local_ent))
1335 return true;
1336 static int last_tick = -1;
1337 static bool phase = false;
1338 if (cmd->tick_count != last_tick) {
1339 last_tick = cmd->tick_count;
1340 phase = !phase;
1341 }
1342 return phase;
1343}
1344
1061bool aimbot_apply(CUserCmd *cmd) { 1345bool aimbot_apply(CUserCmd *cmd) {
1062 unsigned bone_mask = aimbot_bone_mask(); 1346 unsigned bone_mask = aimbot_bone_mask();
1063 if (!bone_mask) 1347 if (!bone_mask)
@@ -1080,7 +1364,8 @@ bool aimbot_apply(CUserCmd *cmd) {
1080 "aimbot", lock, target, point, fov)) 1364 "aimbot", lock, target, point, fov))
1081 return false; 1365 return false;
1082 Vector lead; 1366 Vector lead;
1083 if (projectile_lead_point(target, point, lead)) 1367 if (projectile_lead_point(target, point, lead) &&
1368 lead_point_ok(target, lead, view, max_fov))
1084 point = lead; 1369 point = lead;
1085 QAngle desired = angle_to(g_cam_eye, point); 1370 QAngle desired = angle_to(g_cam_eye, point);
1086 if (punch) { 1371 if (punch) {
@@ -1168,7 +1453,8 @@ bool silent_aim_apply(CUserCmd *cmd) {
1168 "silentaim", lock, target, point, fov)) 1453 "silentaim", lock, target, point, fov))
1169 return false; 1454 return false;
1170 Vector lead; 1455 Vector lead;
1171 if (projectile_lead_point(target, point, lead)) 1456 if (projectile_lead_point(target, point, lead) &&
1457 lead_point_ok(target, lead, view, max_fov))
1172 point = lead; 1458 point = lead;
1173 QAngle desired = angle_to(g_cam_eye, point); 1459 QAngle desired = angle_to(g_cam_eye, point);
1174 if (punch) { 1460 if (punch) {
@@ -1196,7 +1482,8 @@ bool silent_aim_apply(CUserCmd *cmd) {
1196 using SetViewFn = void (*)(void *, QAngle *); 1482 using SetViewFn = void (*)(void *, QAngle *);
1197 vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore); 1483 vfunc<SetViewFn>(g_iface.engine, 20)(g_iface.engine, &restore);
1198 } 1484 }
1199 cmd->buttons |= IN_ATTACK; 1485 if (fire_edge_ok(cmd))
1486 cmd->buttons |= IN_ATTACK;
1200 static int logs = 0; 1487 static int logs = 0;
1201 if (logs < 5) { 1488 if (logs < 5) {
1202 logs++; 1489 logs++;
@@ -1379,7 +1666,7 @@ bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) {
1379 if (!ent) 1666 if (!ent)
1380 continue; 1667 continue;
1381 void *net = entity_networkable(idx); 1668 void *net = entity_networkable(idx);
1382 if (!net) 1669 if (!net || entity_is_dormant(net))
1383 continue; 1670 continue;
1384 const char *cls = entity_class_name(net, nullptr); 1671 const char *cls = entity_class_name(net, nullptr);
1385 if (!cls || !is_npc_class(cls)) 1672 if (!cls || !is_npc_class(cls))
@@ -1417,15 +1704,17 @@ bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) {
1417 maxy = *(float *)(e + g_net.maxs + 4); 1704 maxy = *(float *)(e + g_net.maxs + 4);
1418 maxz = *(float *)(e + g_net.maxs + 8); 1705 maxz = *(float *)(e + g_net.maxs + 8);
1419 } 1706 }
1420 if (!(maxz > minz + 4.0f && maxz < 512.0f)) { 1707 // Reject absurd boxes in both directions; a garbage mins value that is
1708 // hugely negative would otherwise produce an AABB spanning the map.
1709 if (!(minz > -512.0f && maxz > minz + 4.0f && maxz < 512.0f)) {
1421 minz = 0.0f; 1710 minz = 0.0f;
1422 maxz = 72.0f; 1711 maxz = 72.0f;
1423 } 1712 }
1424 if (!(maxx > minx + 1.0f && maxx < 256.0f)) { 1713 if (!(minx > -512.0f && maxx > minx + 1.0f && maxx < 256.0f)) {
1425 minx = -16.0f; 1714 minx = -16.0f;
1426 maxx = 16.0f; 1715 maxx = 16.0f;
1427 } 1716 }
1428 if (!(maxy > miny + 1.0f && maxy < 256.0f)) { 1717 if (!(miny > -512.0f && maxy > miny + 1.0f && maxy < 256.0f)) {
1429 miny = -16.0f; 1718 miny = -16.0f;
1430 maxy = 16.0f; 1719 maxy = 16.0f;
1431 } 1720 }
@@ -1529,7 +1818,8 @@ bool melee_aimbot_apply(CUserCmd *cmd, bool gravity) {
1529 } 1818 }
1530 // Gravity gun primary attack launches the held object (EyeVectors); 1819 // Gravity gun primary attack launches the held object (EyeVectors);
1531 // secondary would drop it instead. 1820 // secondary would drop it instead.
1532 cmd->buttons |= IN_ATTACK; 1821 if (fire_edge_ok(cmd))
1822 cmd->buttons |= IN_ATTACK;
1533 1823
1534 static int logs = 0; 1824 static int logs = 0;
1535 if (logs < 10) { 1825 if (logs < 10) {
@@ -1572,9 +1862,9 @@ static void spread_compute() {
1572 SeedFn seed = (SeedFn)dlsym(RTLD_DEFAULT, "RandomSeed"); 1862 SeedFn seed = (SeedFn)dlsym(RTLD_DEFAULT, "RandomSeed");
1573 FloatFn rf = (FloatFn)dlsym(RTLD_DEFAULT, "RandomFloat"); 1863 FloatFn rf = (FloatFn)dlsym(RTLD_DEFAULT, "RandomFloat");
1574 if (!seed || !rf) { 1864 if (!seed || !rf) {
1575 size_t slash = g_engine.path.find_last_of('/'); 1865 size_t slash = mod_engine().path.find_last_of('/');
1576 if (slash != std::string::npos) { 1866 if (slash != std::string::npos) {
1577 std::string path = g_engine.path.substr(0, slash + 1) + "libvstdlib.so"; 1867 std::string path = mod_engine().path.substr(0, slash + 1) + "libvstdlib.so";
1578 void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); 1868 void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL);
1579 if (!h) 1869 if (!h)
1580 h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL); 1870 h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL);
@@ -1725,38 +2015,49 @@ bool triggerbot_apply(CUserCmd *cmd) {
1725 if (!hit) 2015 if (!hit)
1726 return false; 2016 return false;
1727 2017
1728 using Fn = int (*)(void *); 2018 int idx = entptr_lookup(hit);
1729 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist); 2019 if (idx <= 0) {
1730 if (hi < 0 || hi > 4096) 2020 // Cache miss: ESP is off, or this entity appeared since the last scan.
1731 hi = 4096; 2021 using Fn = int (*)(void *);
1732 for (int idx = 1; idx <= hi; idx++) { 2022 int hi = vfunc<Fn>(g_iface.entitylist, 6)(g_iface.entitylist);
1733 if (entitylist_get(idx) != hit) 2023 if (hi < 0 || hi > 4096)
1734 continue; 2024 hi = 4096;
1735 void *net = entity_networkable(idx); 2025 for (int i = 1; i <= hi; i++) {
1736 const char *cls = net ? entity_class_name(net, nullptr) : nullptr; 2026 if (entitylist_get(i) != hit)
1737 if (!cls || !is_npc_class(cls)) 2027 continue;
1738 return false; 2028 idx = i;
1739 uintptr_t e = (uintptr_t)hit; 2029 entptr_store(hit, i);
1740 int life = g_net.lifestate >= 0 2030 break;
1741 ? (int)*(unsigned char *)(e + g_net.lifestate) 2031 }
1742 : -1; 2032 if (idx <= 0)
1743 if (life != LIFE_ALIVE)
1744 return false;
1745 const char *model = entity_model_name(hit);
1746 bool neutral = false;
1747 if (npc_is_friendly(idx, model, local, neutral) || neutral)
1748 return false;
1749 if (!npc_can_be_damaged(idx, model))
1750 return false; 2033 return false;
2034 }
2035 void *net = entity_networkable(idx);
2036 if (entity_is_dormant(net))
2037 return false;
2038 const char *cls = net ? entity_class_name(net, nullptr) : nullptr;
2039 if (!cls || !is_npc_class(cls))
2040 return false;
2041 uintptr_t e = (uintptr_t)hit;
2042 int life = g_net.lifestate >= 0
2043 ? (int)*(unsigned char *)(e + g_net.lifestate)
2044 : -1;
2045 if (life != LIFE_ALIVE)
2046 return false;
2047 const char *model = entity_model_name(hit);
2048 bool neutral = false;
2049 if (npc_is_friendly(idx, model, local, neutral) || neutral)
2050 return false;
2051 if (!npc_can_be_damaged(idx, model))
2052 return false;
2053 if (fire_edge_ok(cmd))
1751 cmd->buttons |= IN_ATTACK; 2054 cmd->buttons |= IN_ATTACK;
1752 static int logs = 0; 2055 static int logs = 0;
1753 if (logs < 5) { 2056 if (logs < 5) {
1754 logs++; 2057 logs++;
1755 logd("triggerbot: fire idx=%d model=%s", idx, model ? model : "?"); 2058 logd("triggerbot: fire idx=%d model=%s", idx, model ? model : "?");
1756 }
1757 return true;
1758 } 2059 }
1759 return false; 2060 return true;
1760} 2061}
1761 2062
1762// Dynamic props are tinted through their client render colour. StudioRender 2063// Dynamic props are tinted through their client render colour. StudioRender
@@ -2075,13 +2376,8 @@ static void draw_transitions() {
2075 2376
2076void esp_draw() { 2377void esp_draw() {
2077 if (!g_surface || !g_iface.entitylist || !g_net.esp_ok()) return; 2378 if (!g_surface || !g_iface.entitylist || !g_net.esp_ok()) return;
2078 if (!g_scr_w) { 2379 if (!esp_update_screen())
2079 using Fn = void (*)(void *, int &, int &); 2380 return;
2080 vfunc<Fn>(g_surface, SURF_GetScreenSize)(g_surface, g_scr_w, g_scr_h);
2081 logd("ESP: screen %dx%d", g_scr_w, g_scr_h);
2082 if (!g_scr_w) return;
2083 }
2084 draw_fov_circle();
2085 esp_collect(engine_get_local_player()); 2381 esp_collect(engine_get_local_player());
2086 static double last_trans = 0.0; 2382 static double last_trans = 0.0;
2087 double trans_now = esp_now(); 2383 double trans_now = esp_now();
@@ -2805,7 +3101,7 @@ static void items_collect(int local_idx) {
2805 if (!ent) 3101 if (!ent)
2806 continue; 3102 continue;
2807 void *net = entity_networkable(idx); 3103 void *net = entity_networkable(idx);
2808 if (!net) 3104 if (!net || entity_is_dormant(net))
2809 continue; 3105 continue;
2810 const char *cls = entity_class_name(net, nullptr); 3106 const char *cls = entity_class_name(net, nullptr);
2811 const char *scls = nullptr; 3107 const char *scls = nullptr;
@@ -2922,12 +3218,8 @@ void esp_draw_items() {
2922 int local = engine_get_local_player(); 3218 int local = engine_get_local_player();
2923 if (local <= 0) 3219 if (local <= 0)
2924 return; 3220 return;
2925 if (!g_scr_w) { 3221 if (!esp_update_screen())
2926 using Fn = void (*)(void *, int &, int &); 3222 return;
2927 vfunc<Fn>(g_surface, SURF_GetScreenSize)(g_surface, g_scr_w, g_scr_h);
2928 if (!g_scr_w)
2929 return;
2930 }
2931 items_collect(local); 3223 items_collect(local);
2932 3224
2933 int vit_hp = -1, vit_max = -1, vit_armor = -1; 3225 int vit_hp = -1, vit_max = -1, vit_armor = -1;
@@ -2949,7 +3241,11 @@ void esp_draw_items() {
2949 } 3241 }
2950 3242
2951 for (int i = 0; i < g_nitems; i++) { 3243 for (int i = 0; i < g_nitems; i++) {
2952 const ItemTarget &t = g_item_targets[i]; 3244 ItemTarget &t = g_item_targets[i];
3245 // Follow the entity between the 10 Hz discovery scans.
3246 Vector live;
3247 if (entity_origin(entitylist_get(t.idx), live))
3248 t.origin = live;
2953 if (!item_kind_enabled(t.kind)) 3249 if (!item_kind_enabled(t.kind))
2954 continue; 3250 continue;
2955 // Dispensers (chargers, ammo crates, supply crates) stay useful after a 3251 // Dispensers (chargers, ammo crates, supply crates) stay useful after a
@@ -3058,17 +3354,23 @@ static const char *proj_name(int kind) {
3058// Fallback fuse clock when the server detonate time cannot be read. The stock 3354// Fallback fuse clock when the server detonate time cannot be read. The stock
3059// frag fuse is 3 s from the first sighting. 3355// frag fuse is 3 s from the first sighting.
3060static double proj_first_seen(int idx, double now) { 3356static double proj_first_seen(int idx, double now) {
3061 static int idxs[32]; 3357 static constexpr int N = 64;
3062 static double first[32]; 3358 static int idxs[N];
3063 static int n = 0; 3359 static double first[N];
3064 for (int i = 0; i < n; i++) 3360 static int next = 0;
3065 if (idxs[i] == idx) 3361 for (int i = 0; i < N; i++) {
3066 return first[i]; 3362 if (idxs[i] != idx)
3067 if (n < 32) { 3363 continue;
3068 idxs[n] = idx; 3364 // The slot is stale if this index was last seen long ago: a new grenade
3069 first[n] = now; 3365 // reusing the entity number must start its own fuse, not inherit an old
3070 n++; 3366 // one (which produced permanently negative fuses).
3071 } 3367 if (now - first[i] > 10.0)
3368 first[i] = now;
3369 return first[i];
3370 }
3371 idxs[next] = idx;
3372 first[next] = now;
3373 next = (next + 1) % N; // oldest entry is reused instead of dropped
3072 return now; 3374 return now;
3073} 3375}
3074 3376
@@ -3094,7 +3396,7 @@ static void projs_collect(int local_idx) {
3094 if (!ent) 3396 if (!ent)
3095 continue; 3397 continue;
3096 void *net = entity_networkable(idx); 3398 void *net = entity_networkable(idx);
3097 if (!net) 3399 if (!net || entity_is_dormant(net))
3098 continue; 3400 continue;
3099 const char *cls = entity_class_name(net, nullptr); 3401 const char *cls = entity_class_name(net, nullptr);
3100 const char *model = entity_model_name(ent); 3402 const char *model = entity_model_name(ent);
@@ -3171,6 +3473,8 @@ static void draw_grenade_trajectory(int local_idx) {
3171 Vector src{}, vel{}; 3473 Vector src{}, vel{};
3172 bool have = false; 3474 bool have = false;
3173 bool actual = false; 3475 bool actual = false;
3476 // Real blast radius of the grenade being previewed, not a hardcoded 300.
3477 float blast_radius = 300.0f;
3174 // Prefer the local player's thrown frag: the real position and velocity are 3478 // Prefer the local player's thrown frag: the real position and velocity are
3175 // networked, so the preview starts from what is actually flying instead of 3479 // networked, so the preview starts from what is actually flying instead of
3176 // the pre-throw estimate. The bounce contact points still depend on the 3480 // the pre-throw estimate. The bounce contact points still depend on the
@@ -3182,6 +3486,13 @@ static void draw_grenade_trajectory(int local_idx) {
3182 if (!gent) 3486 if (!gent)
3183 continue; 3487 continue;
3184 src = g_projs[i].origin; 3488 src = g_projs[i].origin;
3489 // Live position: the cached origin is up to 100 ms old, and the velocity
3490 // read below is live, so mixing them desynced the arc from the grenade.
3491 Vector live;
3492 if (entity_origin(gent, live))
3493 src = live;
3494 if (g_projs[i].radius > 1.0f)
3495 blast_radius = g_projs[i].radius;
3185 if (g_net.velocity >= 0) { 3496 if (g_net.velocity >= 0) {
3186 const float *pv = (const float *)((uintptr_t)gent + g_net.velocity); 3497 const float *pv = (const float *)((uintptr_t)gent + g_net.velocity);
3187 vel = Vector{pv[0], pv[1], pv[2]}; 3498 vel = Vector{pv[0], pv[1], pv[2]};
@@ -3277,9 +3588,11 @@ static void draw_grenade_trajectory(int local_idx) {
3277 surf_line(x, y - 9, x, y - 4); 3588 surf_line(x, y - 9, x, y - 4);
3278 surf_line(x, y + 4, x, y + 9); 3589 surf_line(x, y + 4, x, y + 9);
3279 } 3590 }
3280 if (landed) { 3591 // The landing ring belongs to the "Blast radius" toggle, and uses the
3592 // networked m_DmgRadius when available instead of a fixed 300.
3593 if (landed && g_proj_radius) {
3281 const int SEG = 36; 3594 const int SEG = 36;
3282 const float radius = 300.0f; // CBaseGrenade m_DmgRadius 3595 const float radius = blast_radius;
3283 surf_color(255, 120, 40, 120); 3596 surf_color(255, 120, 40, 120);
3284 for (int k = 0; k < SEG; k++) { 3597 for (int k = 0; k < SEG; k++) {
3285 float a0 = (float)k * 2.0f * PI_F / (float)SEG; 3598 float a0 = (float)k * 2.0f * PI_F / (float)SEG;
@@ -3298,16 +3611,17 @@ static void draw_grenade_trajectory(int local_idx) {
3298void esp_draw_projectiles() { 3611void esp_draw_projectiles() {
3299 if (!g_surface || !g_iface.entitylist || g_net.origin < 0) 3612 if (!g_surface || !g_iface.entitylist || g_net.origin < 0)
3300 return; 3613 return;
3301 if (!g_scr_w) { 3614 if (!esp_update_screen())
3302 using Fn = void (*)(void *, int &, int &); 3615 return;
3303 vfunc<Fn>(g_surface, SURF_GetScreenSize)(g_surface, g_scr_w, g_scr_h);
3304 if (!g_scr_w)
3305 return;
3306 }
3307 projs_collect(engine_get_local_player()); 3616 projs_collect(engine_get_local_player());
3308 draw_grenade_trajectory(engine_get_local_player()); 3617 draw_grenade_trajectory(engine_get_local_player());
3309 for (int i = 0; i < g_nprojs; i++) { 3618 for (int i = 0; i < g_nprojs; i++) {
3310 const ProjTarget &p = g_projs[i]; 3619 ProjTarget &p = g_projs[i];
3620 // Follow the entity between the 10 Hz discovery scans so the marker and
3621 // the blast-radius ring do not step in 100 ms increments.
3622 Vector live;
3623 if (entity_origin(entitylist_get(p.idx), live))
3624 p.origin = live;
3311 int r = 255, g = 170, b = 60; 3625 int r = 255, g = 170, b = 60;
3312 if (p.kind == PK_RPG) { 3626 if (p.kind == PK_RPG) {
3313 r = 255; g = 120; b = 40; 3627 r = 255; g = 120; b = 40;
diff --git a/src/gui.cpp b/src/gui.cpp
index 454e2cb..8a71ecb 100644
--- a/src/gui.cpp
+++ b/src/gui.cpp
@@ -19,20 +19,8 @@
19// ISurface drawing (same binary slots as menu.cpp / esp.cpp) 19// ISurface drawing (same binary slots as menu.cpp / esp.cpp)
20// --------------------------------------------------------------------------- 20// ---------------------------------------------------------------------------
21 21
22enum { 22// ISurface access lives in overlay.cpp. These thin wrappers keep the GUI code
23 GS_DrawSetColor = 10, 23// readable without repeating the vtable indices in this file.
24 GS_DrawFilledRect = 12,
25 GS_DrawOutlinedRect = 14,
26 GS_DrawLine = 15,
27 GS_DrawSetTextFont = 17,
28 GS_DrawSetTextColor = 18,
29 GS_DrawSetTextPos = 20,
30 GS_DrawPrintText = 22,
31 GS_GetScreenSize = 38,
32 GS_CreateFont = 66,
33 GS_AddGlyphSet = 67,
34 GS_GetTextSize = 75
35};
36 24
37static unsigned long g_font_body = 0; 25static unsigned long g_font_body = 0;
38static unsigned long g_font_title = 0; 26static unsigned long g_font_title = 0;
@@ -43,57 +31,27 @@ static void *g_font_local = (void *)-1;
43static bool g_font_fail_logged = false; 31static bool g_font_fail_logged = false;
44static int g_scr_w = 0, g_scr_h = 0; 32static int g_scr_w = 0, g_scr_h = 0;
45 33
46static void s_color(int r, int g, int b, int a) { 34static void s_color(int r, int g, int b, int a) { overlay_color(r, g, b, a); }
47 using Fn = void (*)(void *, int, int, int, int);
48 vfunc<Fn>(g_surface, GS_DrawSetColor)(g_surface, r, g, b, a);
49}
50static void s_fill(int x0, int y0, int x1, int y1) { 35static void s_fill(int x0, int y0, int x1, int y1) {
51 using Fn = void (*)(void *, int, int, int, int); 36 overlay_fill(x0, y0, x1, y1);
52 vfunc<Fn>(g_surface, GS_DrawFilledRect)(g_surface, x0, y0, x1, y1);
53}
54static void s_outline(int x0, int y0, int x1, int y1) {
55 using Fn = void (*)(void *, int, int, int, int);
56 vfunc<Fn>(g_surface, GS_DrawOutlinedRect)(g_surface, x0, y0, x1, y1);
57} 37}
58static void s_line(int x0, int y0, int x1, int y1) { 38static void s_line(int x0, int y0, int x1, int y1) {
59 using Fn = void (*)(void *, int, int, int, int); 39 overlay_line(x0, y0, x1, y1);
60 vfunc<Fn>(g_surface, GS_DrawLine)(g_surface, x0, y0, x1, y1);
61}
62static void s_text_font(unsigned long f) {
63 using Fn = void (*)(void *, unsigned long);
64 vfunc<Fn>(g_surface, GS_DrawSetTextFont)(g_surface, f);
65} 40}
41static void s_text_font(HFont f) { overlay_text_font(f); }
66static void s_text_color(int r, int g, int b, int a) { 42static void s_text_color(int r, int g, int b, int a) {
67 using Fn = void (*)(void *, int, int, int, int); 43 overlay_text_color(r, g, b, a);
68 vfunc<Fn>(g_surface, GS_DrawSetTextColor)(g_surface, r, g, b, a);
69}
70static void s_text_pos(int x, int y) {
71 using Fn = void (*)(void *, int, int);
72 vfunc<Fn>(g_surface, GS_DrawSetTextPos)(g_surface, x, y);
73} 44}
74static void s_print(const wchar_t *text, int n) { 45static void s_text_pos(int x, int y) { overlay_text_pos(x, y); }
75 using Fn = void (*)(void *, const wchar_t *, int, int); 46static void s_print(const wchar_t *text, int n) { overlay_print_text(text, n); }
76 vfunc<Fn>(g_surface, GS_DrawPrintText)(g_surface, text, n, 0); 47static HFont s_create_font() { return overlay_create_font(); }
77} 48static bool s_set_font(HFont font, const char *name, int tall, int weight,
78static unsigned long s_create_font() {
79 using Fn = unsigned long (*)(void *);
80 return vfunc<Fn>(g_surface, GS_CreateFont)(g_surface);
81}
82static bool s_set_font(unsigned long font, const char *name, int tall, int weight,
83 int flags, int lo, int hi) { 49 int flags, int lo, int hi) {
84 using Fn = bool (*)(void *, unsigned long, const char *, int, int, int, int, 50 return overlay_add_glyph_set(font, name, tall, weight, 0, 0, flags, lo, hi);
85 int, int, int);
86 return vfunc<Fn>(g_surface, GS_AddGlyphSet)(g_surface, font, name, tall,
87 weight, 0, 0, flags, lo, hi);
88}
89static void s_screen(int &w, int &h) {
90 using Fn = void (*)(void *, int &, int &);
91 vfunc<Fn>(g_surface, GS_GetScreenSize)(g_surface, w, h);
92} 51}
93static void s_text_size(unsigned long font, const wchar_t *text, int &w, 52static void s_screen(int &w, int &h) { overlay_screen_size(w, h); }
94 int &h) { 53static void s_text_size(HFont font, const wchar_t *text, int &w, int &h) {
95 using Fn = void (*)(void *, unsigned long, const wchar_t *, int &, int &); 54 overlay_text_size(font, text, w, h);
96 vfunc<Fn>(g_surface, GS_GetTextSize)(g_surface, font, text, w, h);
97} 55}
98 56
99// --------------------------------------------------------------------------- 57// ---------------------------------------------------------------------------
@@ -137,9 +95,12 @@ static void gui_init_sdl() {
137 g_sdl_init_tried = true; 95 g_sdl_init_tried = true;
138 sd_get_keys = (GetKeysFn)dlsym(RTLD_DEFAULT, "SDL_GetKeyboardState"); 96 sd_get_keys = (GetKeysFn)dlsym(RTLD_DEFAULT, "SDL_GetKeyboardState");
139 sd_get_mouse = (GetMouseFn)dlsym(RTLD_DEFAULT, "SDL_GetMouseState"); 97 sd_get_mouse = (GetMouseFn)dlsym(RTLD_DEFAULT, "SDL_GetMouseState");
140 sd_show_cursor = (ShowCursorFn)dlsym(RTLD_DEFAULT, "SDL_ShowCursor"); 98 // RTLD_NEXT, not RTLD_DEFAULT: this object is LD_PRELOADed first, so
141 sd_set_relative = (SetRelativeFn)dlsym(RTLD_DEFAULT, "SDL_SetRelativeMouseMode"); 99 // RTLD_DEFAULT resolves to our own exported wrapper and the "real" pointer
142 sd_get_relative = (GetRelativeFn)dlsym(RTLD_DEFAULT, "SDL_GetRelativeMouseMode"); 100 // would point back into this file.
101 sd_show_cursor = (ShowCursorFn)dlsym(RTLD_NEXT, "SDL_ShowCursor");
102 sd_set_relative = (SetRelativeFn)dlsym(RTLD_NEXT, "SDL_SetRelativeMouseMode");
103 sd_get_relative = (GetRelativeFn)dlsym(RTLD_NEXT, "SDL_GetRelativeMouseMode");
143 sd_add_watch = (AddWatchFn)dlsym(RTLD_DEFAULT, "SDL_AddEventWatch"); 104 sd_add_watch = (AddWatchFn)dlsym(RTLD_DEFAULT, "SDL_AddEventWatch");
144 sd_get_ticks = (GetTicksFn)dlsym(RTLD_DEFAULT, "SDL_GetTicks"); 105 sd_get_ticks = (GetTicksFn)dlsym(RTLD_DEFAULT, "SDL_GetTicks");
145 g_sdl_ready = sd_get_keys && sd_get_mouse; 106 g_sdl_ready = sd_get_keys && sd_get_mouse;
@@ -607,6 +568,10 @@ static int g_mouse_press = 0; // 1..5 on the frame a button goes down
607static unsigned char g_prev_keys[512] = {}; 568static unsigned char g_prev_keys[512] = {};
608static bool g_prev_cursor = true; 569static bool g_prev_cursor = true;
609static bool g_prev_relative = true; 570static bool g_prev_relative = true;
571// Last state the game asked for while the GUI held the mouse; restored on
572// close instead of the (possibly stale) pre-open snapshot.
573static bool g_game_wants_cursor = true;
574static bool g_game_wants_relative = false;
610static double g_hover_since = 0.0; 575static double g_hover_since = 0.0;
611static const char *g_hover_desc = nullptr; 576static const char *g_hover_desc = nullptr;
612static const char *g_hover_prev = nullptr; 577static const char *g_hover_prev = nullptr;
@@ -688,10 +653,12 @@ static unsigned long create_gui_font(int tall) {
688 static const char *families[] = {"Liberation Sans", "DejaVu Sans", 653 static const char *families[] = {"Liberation Sans", "DejaVu Sans",
689 "Noto Sans", "Tahoma", "Verdana", "Arial", 654 "Noto Sans", "Tahoma", "Verdana", "Arial",
690 nullptr}; 655 nullptr};
656 // Create one handle and add glyph sets to it. Creating a handle per family
657 // leaked one HFont per failed family, and ISurface has no DestroyFont.
658 unsigned long f = s_create_font();
659 if (!f)
660 return 0;
691 for (int i = 0; families[i]; i++) { 661 for (int i = 0; families[i]; i++) {
692 unsigned long f = s_create_font();
693 if (!f)
694 break;
695 if (s_set_font(f, families[i], tall, 400, 0x400, 0x20, 0xFF) && 662 if (s_set_font(f, families[i], tall, 400, 0x400, 0x20, 0xFF) &&
696 font_valid(f)) { 663 font_valid(f)) {
697 logd("gui: font %dpx family %s", tall, families[i]); 664 logd("gui: font %dpx family %s", tall, families[i]);
@@ -710,8 +677,13 @@ static void ensure_fonts() {
710 } 677 }
711 if (le != g_font_local) { 678 if (le != g_font_local) {
712 g_font_local = le; 679 g_font_local = le;
713 gui_reset_fonts(); 680 // VGUI fonts are global; a level change alone does not invalidate them.
714 logd("gui: fonts reset (local entity change)"); 681 // Dropping valid handles here leaked a fresh pair on every death, respawn
682 // and map load. Only reset when the manager actually lost them.
683 if (!font_valid(g_font_body) || !font_valid(g_font_title)) {
684 gui_reset_fonts();
685 logd("gui: fonts reset (invalid after local entity change)");
686 }
715 } 687 }
716 688
717 bool scale_changed = fabsf(g_font_scale - g_scale) >= 0.001f; 689 bool scale_changed = fabsf(g_font_scale - g_scale) >= 0.001f;
@@ -871,20 +843,30 @@ static void draw_text_raw(int x, int y, const char *s, unsigned long font,
871 843
872static void text_in(const Rect &clip, int x, int y, const char *s, 844static void text_in(const Rect &clip, int x, int y, const char *s,
873 unsigned long font, int tall, int max_w, const Col &c) { 845 unsigned long font, int tall, int max_w, const Col &c) {
874 (void)max_w; // callers must size the layout; text is never truncated
875 if (!s || !font) 846 if (!s || !font)
876 return; 847 return;
877 if (y < clip.y || y + tall > clip.y + clip.h) 848 if (y < clip.y || y + tall > clip.y + clip.h)
878 return; 849 return;
879 if (x >= clip.x + clip.w) 850 if (x >= clip.x + clip.w)
880 return; 851 return;
881 int w = text_w(s, tall); 852 // Available width: the clip edge, further limited by the layout's column
882 if (x + w > clip.x + clip.w) 853 // width. Overflowing text used to be shifted left over its neighbour; now it
883 x = clip.x + clip.w - w; 854 // is truncated with an ellipsis instead.
884 if (x < clip.x) 855 int avail = clip.x + clip.w - x;
885 x = clip.x; 856 if (max_w > 0 && max_w < avail)
857 avail = max_w;
858 if (avail <= 0)
859 return;
860 std::string out = s;
861 if (text_w(out.c_str(), tall) > avail) {
862 while (!out.empty() && text_w((out + "...").c_str(), tall) > avail)
863 out.pop_back();
864 if (out.empty())
865 return;
866 out += "...";
867 }
886 s_text_color(c.r, c.g, c.b, c.a); 868 s_text_color(c.r, c.g, c.b, c.a);
887 draw_text_raw(x, y, s, font, tall); 869 draw_text_raw(x, y, out.c_str(), font, tall);
888} 870}
889 871
890static Rect clip_rect(const Rect &r, const Rect &clip) { 872static Rect clip_rect(const Rect &r, const Rect &clip) {
@@ -911,13 +893,49 @@ static void fill(const Rect &r, const Col &c, const Rect *clip = nullptr) {
911static void outline(const Rect &r, const Col &c, const Rect *clip = nullptr) { 893static void outline(const Rect &r, const Col &c, const Rect *clip = nullptr) {
912 if (r.w <= 0 || r.h <= 0) 894 if (r.w <= 0 || r.h <= 0)
913 return; 895 return;
914 Rect q = clip ? clip_rect(r, *clip) : r; 896 // Draw the four edges as thin filled rects. Outlining the *clamped* rect used
915 if (q.w <= 0 || q.h <= 0) 897 // to paint a false border along the clip edge whenever a card was partially
916 return; 898 // scrolled out of the content pane.
917 s_color(c.r, c.g, c.b, c.a); 899 int t = S(1.0f);
918 s_outline(q.x, q.y, q.x + q.w, q.y + q.h); 900 if (t < 1) t = 1;
919} 901 fill(Rect{r.x, r.y, r.w, t}, c, clip);
920static void line(int x0, int y0, int x1, int y1, const Col &c) { 902 fill(Rect{r.x, r.y + r.h - t, r.w, t}, c, clip);
903 fill(Rect{r.x, r.y, t, r.h}, c, clip);
904 fill(Rect{r.x + r.w - t, r.y, t, r.h}, c, clip);
905}
906// Liang–Barsky line clip. `clip` is optional so the existing call sites that
907// draw inside an already-safe area keep working unchanged.
908static void line(int x0, int y0, int x1, int y1, const Col &c,
909 const Rect *clip = nullptr) {
910 if (clip) {
911 float dx = (float)(x1 - x0), dy = (float)(y1 - y0);
912 float t0 = 0.0f, t1 = 1.0f;
913 float p[4] = {-dx, dx, -dy, dy};
914 float q[4] = {(float)(x0 - clip->x),
915 (float)(clip->x + clip->w - x0),
916 (float)(y0 - clip->y),
917 (float)(clip->y + clip->h - y0)};
918 for (int i = 0; i < 4; i++) {
919 if (fabsf(p[i]) < 1e-6f) {
920 if (q[i] < 0.0f)
921 return; // parallel and outside
922 continue;
923 }
924 float t = q[i] / p[i];
925 if (p[i] < 0.0f) {
926 if (t > t1) return;
927 if (t > t0) t0 = t;
928 } else {
929 if (t < t0) return;
930 if (t < t1) t1 = t;
931 }
932 }
933 int nx0 = (int)((float)x0 + t0 * dx + 0.5f);
934 int ny0 = (int)((float)y0 + t0 * dy + 0.5f);
935 int nx1 = (int)((float)x0 + t1 * dx + 0.5f);
936 int ny1 = (int)((float)y0 + t1 * dy + 0.5f);
937 x0 = nx0; y0 = ny0; x1 = nx1; y1 = ny1;
938 }
921 s_color(c.r, c.g, c.b, c.a); 939 s_color(c.r, c.g, c.b, c.a);
922 s_line(x0, y0, x1, y1); 940 s_line(x0, y0, x1, y1);
923} 941}
@@ -1298,7 +1316,7 @@ static void draw_picker(const Rect &clip, const Rect &cr, const Ctrl &ct,
1298 outline(p.alpha, Col{0, 0, 0, 255}, &clip); 1316 outline(p.alpha, Col{0, 0, 0, 255}, &clip);
1299 int amx = p.alpha.x + (int)((float)pa / 255.0f * (float)p.alpha.w); 1317 int amx = p.alpha.x + (int)((float)pa / 255.0f * (float)p.alpha.w);
1300 line(amx, p.alpha.y - S(2.0f), amx, p.alpha.y + p.alpha.h + S(2.0f), 1318 line(amx, p.alpha.y - S(2.0f), amx, p.alpha.y + p.alpha.h + S(2.0f),
1301 Col{255, 255, 255, 255}); 1319 Col{255, 255, 255, 255}, &clip);
1302 } 1320 }
1303 1321
1304 // Selection markers. 1322 // Selection markers.
@@ -1311,7 +1329,7 @@ static void draw_picker(const Rect &clip, const Rect &cr, const Ctrl &ct,
1311 1329
1312 int hxm = p.hue.x + (int)((ct.hue / 360.0f) * (float)p.hue.w); 1330 int hxm = p.hue.x + (int)((ct.hue / 360.0f) * (float)p.hue.w);
1313 line(hxm, p.hue.y - S(2.0f), hxm, p.hue.y + p.hue.h + S(2.0f), 1331 line(hxm, p.hue.y - S(2.0f), hxm, p.hue.y + p.hue.h + S(2.0f),
1314 Col{255, 255, 255, 255}); 1332 Col{255, 255, 255, 255}, &clip);
1315 1333
1316 // Preview swatch and hex value. 1334 // Preview swatch and hex value.
1317 draw_checker(p.preview, &clip); 1335 draw_checker(p.preview, &clip);
@@ -1351,6 +1369,23 @@ static Rect card_rect(int index, int y) {
1351 return Rect{x, y, w, card_h(c)}; 1369 return Rect{x, y, w, card_h(c)};
1352} 1370}
1353 1371
1372// Slider track inside a card: the single source of truth for drawing,
1373// hit-testing and dragging. The drag path used to recompute this by hand and
1374// omitted the card's +S(10) left margin, so the value jumped on mouse-down.
1375static Rect slider_track(const Rect &cr, int y, int rh) {
1376 int w = cr.w - S(16.0f) - S(230.0f) - S(64.0f);
1377 if (w < S(30.0f)) w = S(30.0f);
1378 return Rect{cr.x + S(16.0f) + S(230.0f), y + (rh - S(8.0f)) / 2, w,
1379 S(8.0f)};
1380}
1381
1382static int card_for_ctrl(int ci) {
1383 for (int i = 0; i < g_ncards; i++)
1384 if (ci >= g_cards[i].c0 && ci < g_cards[i].c0 + g_cards[i].n)
1385 return i;
1386 return -1;
1387}
1388
1354static bool card_in_category(int i) { return g_cards[i].cat == g_cat; } 1389static bool card_in_category(int i) { return g_cards[i].cat == g_cat; }
1355 1390
1356static int content_height() { 1391static int content_height() {
@@ -1379,10 +1414,13 @@ static void close_gui() {
1379 g_reset_armed = false; 1414 g_reset_armed = false;
1380 input_clear_mouse_states(); 1415 input_clear_mouse_states();
1381 if (g_sdl_ready) { 1416 if (g_sdl_ready) {
1417 // Restore what the game actually wants now, not the pre-open snapshot: the
1418 // game keeps requesting relative mode/cursor state while the GUI is open
1419 // (menu -> map transition), and those requests were being dropped.
1382 if (sd_set_relative) 1420 if (sd_set_relative)
1383 sd_set_relative(g_prev_relative ? 1 : 0); 1421 sd_set_relative(g_game_wants_relative ? 1 : 0);
1384 if (sd_show_cursor) 1422 if (sd_show_cursor)
1385 sd_show_cursor(g_prev_cursor ? 1 : 0); 1423 sd_show_cursor(g_game_wants_cursor ? 1 : 0);
1386 } 1424 }
1387 config_save(); 1425 config_save();
1388 logf("gui: closed"); 1426 logf("gui: closed");
@@ -1410,6 +1448,9 @@ static void open_gui() {
1410 g_prev_relative = sd_get_relative() != 0; 1448 g_prev_relative = sd_get_relative() != 0;
1411 if (sd_show_cursor) 1449 if (sd_show_cursor)
1412 g_prev_cursor = sd_show_cursor(-1) != 0; 1450 g_prev_cursor = sd_show_cursor(-1) != 0;
1451 // Until the game asks for something else, restore what it had before.
1452 g_game_wants_relative = g_prev_relative;
1453 g_game_wants_cursor = g_prev_cursor;
1413 if (sd_set_relative) 1454 if (sd_set_relative)
1414 sd_set_relative(0); 1455 sd_set_relative(0);
1415 if (sd_show_cursor) 1456 if (sd_show_cursor)
@@ -1519,6 +1560,10 @@ static void handle_mouse_click() {
1519 } 1560 }
1520 } 1561 }
1521 g_dropdown_ctrl = -1; 1562 g_dropdown_ctrl = -1;
1563 // Consume the dismissal click: it must not also activate whatever control
1564 // happened to be underneath the popup.
1565 g_mouse_click = false;
1566 return;
1522 } 1567 }
1523 1568
1524 // title bar: drag 1569 // title bar: drag
@@ -1624,9 +1669,7 @@ static void handle_mouse_click() {
1624 return; 1669 return;
1625 } 1670 }
1626 if (ct.kind == CK_SLIDER) { 1671 if (ct.kind == CK_SLIDER) {
1627 Rect track{cr.x + S(16.0f) + S(230.0f), 1672 Rect track = slider_track(cr, by, rh);
1628 by + (rh - S(8.0f)) / 2,
1629 cr.w - S(16.0f) - S(230.0f) - S(64.0f), S(8.0f)};
1630 if (mx >= track.x && mx < track.x + track.w) { 1673 if (mx >= track.x && mx < track.x + track.w) {
1631 float t = (float)(mx - track.x) / (float)track.w; 1674 float t = (float)(mx - track.x) / (float)track.w;
1632 int v = ct.min + (int)(t * (ct.max - ct.min) + 0.5f); 1675 int v = ct.min + (int)(t * (ct.max - ct.min) + 0.5f);
@@ -1717,6 +1760,11 @@ static void handle_mouse_click() {
1717static void handle_hover() { 1760static void handle_hover() {
1718 g_hover_desc = nullptr; 1761 g_hover_desc = nullptr;
1719 g_hover_slider = -1; 1762 g_hover_slider = -1;
1763 // An open popup/picker or an active drag owns the cursor; arming hover on the
1764 // controls underneath let Left/Right arrows adjust a hidden slider.
1765 if (g_dropdown_ctrl >= 0 || g_picker_ctrl >= 0 || g_picker_drag ||
1766 g_slider_drag >= 0 || g_dragging || g_scroll_drag)
1767 return;
1720 int mx = g_mouse_x, my = g_mouse_y; 1768 int mx = g_mouse_x, my = g_mouse_y;
1721 Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f), 1769 Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f),
1722 g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)}; 1770 g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)};
@@ -1849,14 +1897,15 @@ void gui_handle_input() {
1849 config_save(); 1897 config_save();
1850 } else { 1898 } else {
1851 Ctrl &ct = g_ctrls[g_slider_drag]; 1899 Ctrl &ct = g_ctrls[g_slider_drag];
1852 Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f), 1900 int ci = card_for_ctrl(g_slider_drag);
1853 g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)}; 1901 if (ci >= 0) {
1854 int x0 = g_win_x + S(190.0f) + S(16.0f) + S(230.0f); 1902 Rect track = slider_track(card_rect(ci, 0), 0, 0);
1855 int x1 = g_win_x + g_win_w - S(28.0f) - S(64.0f); 1903 float t = (float)(g_mouse_x - track.x) / (float)track.w;
1856 (void)content; 1904 if (t < 0.0f) t = 0.0f;
1857 float t = (float)(g_mouse_x - x0) / (float)(x1 - x0); 1905 if (t > 1.0f) t = 1.0f;
1858 int v = ct.min + (int)(t * (ct.max - ct.min) + 0.5f); 1906 int v = ct.min + (int)(t * (ct.max - ct.min) + 0.5f);
1859 ctrl_set(ct, v); 1907 ctrl_set(ct, v);
1908 }
1860 } 1909 }
1861 } 1910 }
1862 1911
@@ -1938,18 +1987,15 @@ static void draw_checkbox(int x, int y, bool on,
1938 fill(box, on ? C_ACCENT : Col{30, 34, 42, 255}, clip); 1987 fill(box, on ? C_ACCENT : Col{30, 34, 42, 255}, clip);
1939 outline(box, C_BORDER, clip); 1988 outline(box, C_BORDER, clip);
1940 if (on) { 1989 if (on) {
1941 line(x + S(3.0f), y + S(8.0f), x + S(7.0f), y + S(12.0f), C_BG); 1990 line(x + S(3.0f), y + S(8.0f), x + S(7.0f), y + S(12.0f), C_BG, clip);
1942 line(x + S(7.0f), y + S(12.0f), x + S(13.0f), y + S(4.0f), C_BG); 1991 line(x + S(7.0f), y + S(12.0f), x + S(13.0f), y + S(4.0f), C_BG, clip);
1943 } 1992 }
1944} 1993}
1945 1994
1946static void draw_slider(const Rect &clip, const Rect &cr, int y, int rh, 1995static void draw_slider(const Rect &clip, const Rect &cr, int y, int rh,
1947 const Ctrl &ct) { 1996 const Ctrl &ct) {
1948 int track_x = cr.x + S(16.0f) + S(230.0f); 1997 Rect track = slider_track(cr, y, rh);
1949 int track_w = cr.w - S(16.0f) - S(230.0f) - S(64.0f); 1998 int track_w = track.w;
1950 if (track_w < S(30.0f)) track_w = S(30.0f);
1951 int track_y = y + (rh - S(8.0f)) / 2;
1952 Rect track{track_x, track_y, track_w, S(8.0f)};
1953 fill(track, Col{38, 42, 50, 255}, &clip); 1999 fill(track, Col{38, 42, 50, 255}, &clip);
1954 int value = ctrl_val(ct); 2000 int value = ctrl_val(ct);
1955 float t = (ct.max > ct.min) 2001 float t = (ct.max > ct.min)
@@ -2416,6 +2462,8 @@ extern "C" __attribute__((visibility("default"))) int SDL_ShowCursor(int toggle)
2416 if (!real_show_cursor) 2462 if (!real_show_cursor)
2417 real_show_cursor = (int (*)(int))dlsym(RTLD_NEXT, "SDL_ShowCursor"); 2463 real_show_cursor = (int (*)(int))dlsym(RTLD_NEXT, "SDL_ShowCursor");
2418 if (g_open && toggle != -1) { 2464 if (g_open && toggle != -1) {
2465 // Remember what the game asked for so close_gui() can restore it.
2466 g_game_wants_cursor = toggle != 0;
2419 if (real_show_cursor) 2467 if (real_show_cursor)
2420 real_show_cursor(1); 2468 real_show_cursor(1);
2421 return 1; 2469 return 1;
@@ -2429,6 +2477,7 @@ SDL_SetRelativeMouseMode(int enabled) {
2429 real_set_relative = 2477 real_set_relative =
2430 (int (*)(int))dlsym(RTLD_NEXT, "SDL_SetRelativeMouseMode"); 2478 (int (*)(int))dlsym(RTLD_NEXT, "SDL_SetRelativeMouseMode");
2431 if (g_open) { 2479 if (g_open) {
2480 g_game_wants_relative = enabled != 0;
2432 if (real_set_relative) 2481 if (real_set_relative)
2433 real_set_relative(0); 2482 real_set_relative(0);
2434 return 0; 2483 return 0;
@@ -2520,7 +2569,11 @@ SDL_PollEvent(void *event) {
2520 unsigned int type = *(unsigned int *)event; 2569 unsigned int type = *(unsigned int *)event;
2521 if (type != 0x400) // SDL_MOUSEMOTION 2570 if (type != 0x400) // SDL_MOUSEMOTION
2522 return r; 2571 return r;
2523 // Drop motion while the GUI is open: the game must not accumulate look 2572 // Keep the event (VGUI hover and SDL's internal cursor state need it) but
2524 // deltas. The GUI uses absolute SDL_GetMouseState polling. 2573 // zero the relative deltas so the game does not accumulate look movement.
2574 // SDL_MouseMotionEvent: xrel at +28, yrel at +32.
2575 *(int *)((char *)event + 28) = 0;
2576 *(int *)((char *)event + 32) = 0;
2577 return r;
2525 } 2578 }
2526} 2579}
diff --git a/src/hl2h.cpp b/src/hl2h.cpp
index ddfc02a..6697778 100644
--- a/src/hl2h.cpp
+++ b/src/hl2h.cpp
@@ -1,5 +1,6 @@
1// hl2h.cpp — core: logging, vector math, globals, feature logic, setup 1// hl2h.cpp — core: logging, vector math, globals, feature logic, setup
2#include "hl2h.h" 2#include "hl2h.h"
3#include <atomic>
3#include <cmath> 4#include <cmath>
4#include <cstdarg> 5#include <cstdarg>
5#include <cstdlib> 6#include <cstdlib>
@@ -7,6 +8,41 @@
7#include <time.h> 8#include <time.h>
8 9
9// --------------------------------------------------------------------------- 10// ---------------------------------------------------------------------------
11// init readiness
12// ---------------------------------------------------------------------------
13
14static std::atomic<int> g_init_stage{INIT_NONE};
15
16void init_set_stage(int stage) {
17 g_init_stage.store(stage, std::memory_order_release);
18}
19
20int init_stage() { return g_init_stage.load(std::memory_order_acquire); }
21
22// ---------------------------------------------------------------------------
23// vtable cloning
24// ---------------------------------------------------------------------------
25//
26// Under the Itanium C++ ABI the two words before slot 0 are offset-to-top and
27// the type_info pointer. Copying only the slots and repointing the object's
28// vptr leaves those words pointing at allocator metadata, which breaks any
29// RTTI the host performs on the interface. Keep the header words with the
30// clone and hand back slot 0.
31
32void **vtable_clone(void *iface, size_t slots) {
33 if (!iface || slots == 0)
34 return nullptr;
35 void **vt = *(void ***)iface;
36 if (!vt)
37 return nullptr;
38 void **out = new void *[slots + 2];
39 out[0] = vt[-2];
40 out[1] = vt[-1];
41 memcpy(out + 2, vt, slots * sizeof(void *));
42 return out + 2;
43}
44
45// ---------------------------------------------------------------------------
10// logging 46// logging
11// --------------------------------------------------------------------------- 47// ---------------------------------------------------------------------------
12 48
@@ -67,7 +103,6 @@ void logd(const char *fmt, ...) {
67// globals 103// globals
68// --------------------------------------------------------------------------- 104// ---------------------------------------------------------------------------
69 105
70Module g_client, g_engine, g_server;
71GameInterfaces g_iface; 106GameInterfaces g_iface;
72NetVars g_net; 107NetVars g_net;
73Vector g_cam_eye{0, 0, 0}; 108Vector g_cam_eye{0, 0, 0};
@@ -87,11 +122,11 @@ QAngle vector_angles(const Vector &forward) {
87 a.y = atan2f(forward.y, forward.x) * 180.f / PI_F; 122 a.y = atan2f(forward.y, forward.x) * 180.f / PI_F;
88 if (a.y < 0) 123 if (a.y < 0)
89 a.y += 360.f; 124 a.y += 360.f;
125 // Pitch stays in [-90, 90] like Source's QAngle; wrapping negatives to
126 // [270, 360) made every upward aim compare as a steeply negative angle.
90 a.x = atan2f(-forward.z, 127 a.x = atan2f(-forward.z,
91 sqrtf(forward.x * forward.x + forward.y * forward.y)) * 128 sqrtf(forward.x * forward.x + forward.y * forward.y)) *
92 180.f / PI_F; 129 180.f / PI_F;
93 if (a.x < 0)
94 a.x += 360.f;
95 } 130 }
96 return a; 131 return a;
97} 132}
@@ -485,7 +520,31 @@ static bool g_seed_hook_installed = false;
485// Pellet-spread window armed by the player's FireBullets RandomSeed call. 520// Pellet-spread window armed by the player's FireBullets RandomSeed call.
486static volatile int g_spread_rand_left = 0; 521static volatile int g_spread_rand_left = 0;
487 522
523// Identify the caller of a random-stream function by reading the caller's
524// stack frame. The callers live in Valve's binaries, which are not built with
525// the guarantee our own -fno-omit-frame-pointer gives us, so treat the walk as
526// untrusted: it may only stay inside the current stack, and the object it
527// yields is used for nothing but comparison against g_local_server_player.
528static void *caller_self_arg() {
529 void *sp = nullptr, *bp = nullptr;
530 __asm__ volatile("movl %%esp, %0" : "=r"(sp));
531 __asm__ volatile("movl %%ebp, %0" : "=r"(bp));
532 uintptr_t spv = (uintptr_t)sp, bpv = (uintptr_t)bp;
533 constexpr uintptr_t MAX_DEPTH = 0x100000; // 1 MiB of stack frames
534 if (!bpv || (bpv & 3) || bpv <= spv || bpv - spv > MAX_DEPTH)
535 return nullptr;
536 uintptr_t caller = *(uintptr_t *)bpv;
537 if (!caller || (caller & 3) || caller < bpv || caller - spv > MAX_DEPTH)
538 return nullptr;
539 return *(void **)(caller + 8); // first argument slot of the caller
540}
541
488static void hkdSrvRandomSeed(int seed) { 542static void hkdSrvRandomSeed(int seed) {
543 if (!init_live()) {
544 if (g_real_random_seed)
545 g_real_random_seed(seed);
546 return;
547 }
489 uintptr_t ret = (uintptr_t)__builtin_return_address(0); 548 uintptr_t ret = (uintptr_t)__builtin_return_address(0);
490 long off = (long)(ret - g_srv_base); 549 long off = (long)(ret - g_srv_base);
491 static bool trace = getenv("HL2H_SEED_TRACE") != nullptr; 550 static bool trace = getenv("HL2H_SEED_TRACE") != nullptr;
@@ -494,16 +553,16 @@ static void hkdSrvRandomSeed(int seed) {
494 // The caller's first argument is `this`. Only FireBullets is called on the 553 // The caller's first argument is `this`. Only FireBullets is called on the
495 // player entity; the CUniformRandomStream wrappers and the constructor-time 554 // player entity; the CUniformRandomStream wrappers and the constructor-time
496 // RandomSeed(0) reset have a different object there. 555 // RandomSeed(0) reset have a different object there.
497 void *frame = nullptr; 556 void *self = caller_self_arg();
498 __asm__ volatile("movl %%ebp, %0" : "=r"(frame));
499 if (frame)
500 frame = *(void **)frame; // caller's ebp (CBaseEntity::FireBullets)
501 void *self = frame ? *(void **)((char *)frame + 8) : nullptr;
502 bool local = self && g_local_server_player && 557 bool local = self && g_local_server_player &&
503 (uintptr_t)self == g_local_server_player; 558 (uintptr_t)self == g_local_server_player;
504 559
505 if (local && g_server.contains(ret) && feature_active(FEAT_SPREAD) && 560 // Seed selection and engine spread zeroing are independent toggles: "engine
506 spread_no_spread_enabled() && spread_engine_enabled()) { 561 // override" only decides whether RandomFloat is forced to 0, it must not
562 // disable the seed hook (without the seed hook the server ignores
563 // cmd->random_seed entirely).
564 if (local && mod_server().contains(ret) && feature_active(FEAT_SPREAD) &&
565 spread_no_spread_enabled()) {
507 g_spread_rand_left = 8; 566 g_spread_rand_left = 8;
508 int pellets = g_spread_pellets; 567 int pellets = g_spread_pellets;
509 if (pellets < 1) 568 if (pellets < 1)
@@ -533,12 +592,12 @@ static void hkdSrvRandomSeed(int seed) {
533 592
534static void install_seed_hook() { 593static void install_seed_hook() {
535 static bool failed = false; 594 static bool failed = false;
536 if (g_seed_hook_installed || failed || !g_server.valid()) 595 if (g_seed_hook_installed || failed || !mod_server().valid())
537 return; 596 return;
538 g_srv_base = g_server.ranges[0].lo; 597 g_srv_base = mod_server().ranges[0].lo;
539 uintptr_t reloc = 0, bias = 0; 598 uintptr_t reloc = 0, bias = 0;
540 uintptr_t slot = 0; 599 uintptr_t slot = 0;
541 if (module_jump_slot(g_server.path.c_str(), "RandomSeed", &reloc, &bias)) { 600 if (module_jump_slot(mod_server().path.c_str(), "RandomSeed", &reloc, &bias)) {
542 slot = g_srv_base + reloc - bias; 601 slot = g_srv_base + reloc - bias;
543 logf("seed hook: RandomSeed GOT reloc=%#lx bias=%#lx -> %#lx", 602 logf("seed hook: RandomSeed GOT reloc=%#lx bias=%#lx -> %#lx",
544 (unsigned long)reloc, (unsigned long)bias, (unsigned long)slot); 603 (unsigned long)reloc, (unsigned long)bias, (unsigned long)slot);
@@ -547,13 +606,13 @@ static void install_seed_hook() {
547 static const uintptr_t known[] = {0xce5348, 0xce1348}; 606 static const uintptr_t known[] = {0xce5348, 0xce1348};
548 for (size_t i = 0; i < sizeof(known) / sizeof(known[0]) && !slot; i++) { 607 for (size_t i = 0; i < sizeof(known) / sizeof(known[0]) && !slot; i++) {
549 uintptr_t cand = g_srv_base + known[i]; 608 uintptr_t cand = g_srv_base + known[i];
550 if (g_server.contains(cand)) 609 if (mod_server().contains(cand))
551 slot = cand; 610 slot = cand;
552 } 611 }
553 logf("seed hook: ELF reloc scan failed, fallback slot=%#lx", 612 logf("seed hook: ELF reloc scan failed, fallback slot=%#lx",
554 (unsigned long)slot); 613 (unsigned long)slot);
555 } 614 }
556 if (!slot || !g_server.contains(slot)) { 615 if (!slot || !mod_server().contains(slot)) {
557 failed = true; 616 failed = true;
558 logf("seed hook: no writable RandomSeed slot"); 617 logf("seed hook: no writable RandomSeed slot");
559 return; 618 return;
@@ -568,8 +627,15 @@ static void install_seed_hook() {
568 toastf(TOAST_WARN, "Spread seed hook unavailable"); 627 toastf(TOAST_WARN, "Spread seed hook unavailable");
569 return; 628 return;
570 } 629 }
571 mprotect((void *)(slot & ~(uintptr_t)0xFFF), 0x1000, 630 // The slot is 4-byte aligned, so a single page always covers the 4-byte
572 PROT_READ | PROT_WRITE | PROT_EXEC); 631 // write. No PROT_EXEC: this is a data slot. Permissions are intentionally
632 // not restored because .got.plt stays writable for lazy binding.
633 uintptr_t page = slot & ~(uintptr_t)0xFFF;
634 if (mprotect((void *)page, 0x1000, PROT_READ | PROT_WRITE) != 0) {
635 failed = true;
636 logf("seed hook: mprotect failed for page %#lx", (unsigned long)page);
637 return;
638 }
573 *(uint32_t *)slot = (uint32_t)(uintptr_t)&hkdSrvRandomSeed; 639 *(uint32_t *)slot = (uint32_t)(uintptr_t)&hkdSrvRandomSeed;
574 g_seed_hook_installed = true; 640 g_seed_hook_installed = true;
575 logf("seed hook: server GOT %#lx -> %p (real %p)", 641 logf("seed hook: server GOT %#lx -> %p (real %p)",
@@ -598,19 +664,16 @@ static bool g_uniform_hook_installed = false;
598// (the compiler may outline it), so this window is the reliable signal. 664// (the compiler may outline it), so this window is the reliable signal.
599 665
600static float hkdUniformRandomFloat(void *self, float mn, float mx) { 666static float hkdUniformRandomFloat(void *self, float mn, float mx) {
667 if (!init_live())
668 return g_orig_uniform_float ? g_orig_uniform_float(self, mn, mx) : mn;
601 bool enabled = g_local_server_player && feature_active(FEAT_SPREAD) && 669 bool enabled = g_local_server_player && feature_active(FEAT_SPREAD) &&
602 spread_no_spread_enabled() && spread_engine_enabled(); 670 spread_no_spread_enabled() && spread_engine_enabled();
603 bool hit = false; 671 bool hit = false;
604 void *who = nullptr; 672 void *who = nullptr;
605 if (enabled) { 673 if (enabled) {
606 void *frame = nullptr; 674 who = caller_self_arg();
607 __asm__ volatile("movl %%ebp, %0" : "=r"(frame)); 675 if (who && (uintptr_t)who == g_local_server_player)
608 if (frame) { 676 hit = true;
609 frame = *(void **)frame; // caller's ebp
610 who = frame ? *(void **)((char *)frame + 8) : nullptr;
611 if (who && (uintptr_t)who == g_local_server_player)
612 hit = true;
613 }
614 if (!hit && g_spread_rand_left > 0) { 677 if (!hit && g_spread_rand_left > 0) {
615 // The next few draws after the player's FireBullets RandomSeed are the 678 // The next few draws after the player's FireBullets RandomSeed are the
616 // pellet spread, regardless of which function emits them. 679 // pellet spread, regardless of which function emits them.
@@ -748,9 +811,26 @@ static bool edge_jump_apply(CUserCmd *cmd, void *ent, int flags,
748 return true; 811 return true;
749} 812}
750 813
814// Everything tied to one map load / local player lifetime. Called when the
815// local entity pointer changes (map load, respawn, disconnect).
816static void session_reset() {
817 bones_clear();
818 aimbot_reset_lock();
819 silentaim_reset_lock();
820 esp_reset_session();
821 fullbright_invalidate();
822 world_tint_invalidate();
823 fog_invalidate();
824 skybox_invalidate();
825 logd("session: reset (local entity changed)");
826}
827
751static void do_move(int seq, float frametime) { 828static void do_move(int seq, float frametime) {
752 install_seed_hook(); 829 install_seed_hook();
753 install_uniform_random_hook(); 830 install_uniform_random_hook();
831 // The pellet window is armed inside FireBullets during this tick; it must
832 // never survive into the next one.
833 g_spread_rand_left = 0;
754 static bool g_echo_sent = false; 834 static bool g_echo_sent = false;
755 if (!g_input || !g_net.ok()) 835 if (!g_input || !g_net.ok())
756 return; 836 return;
@@ -779,17 +859,13 @@ static void do_move(int seq, float frametime) {
779 859
780 view_settings_apply(ent); 860 view_settings_apply(ent);
781 861
782 // New map / respawn: drop stale bone caches and target locks. 862 // New map / respawn: drop stale caches and locks. Any per-map state must be
863 // cleared here; caches that are not listed will leak across level changes
864 // (that is how the server-health latch and the fuse table broke before).
783 static void *last_local_ent = nullptr; 865 static void *last_local_ent = nullptr;
784 if (ent != last_local_ent) { 866 if (ent != last_local_ent) {
785 last_local_ent = ent; 867 last_local_ent = ent;
786 bones_clear(); 868 session_reset();
787 aimbot_reset_lock();
788 silentaim_reset_lock();
789 fullbright_invalidate();
790 world_tint_invalidate();
791 fog_invalidate();
792 skybox_invalidate();
793 } 869 }
794 870
795 if (feature_active(FEAT_SPREAD)) { 871 if (feature_active(FEAT_SPREAD)) {
@@ -854,8 +930,9 @@ static void do_move(int seq, float frametime) {
854 930
855 if ((bhop_on || abh_on) && (cmd->buttons & IN_JUMP)) { 931 if ((bhop_on || abh_on) && (cmd->buttons & IN_JUMP)) {
856 if (inwater && (cmd->buttons & IN_DUCK)) { 932 if (inwater && (cmd->buttons & IN_DUCK)) {
933 // Not a bhop tick, but combat and jump assist still have to run.
857 lastground = onground; 934 lastground = onground;
858 return; 935 goto bhop_done;
859 } 936 }
860 float speed = sqrtf(vel[0] * vel[0] + vel[1] * vel[1] + vel[2] * vel[2]); 937 float speed = sqrtf(vel[0] * vel[0] + vel[1] * vel[1] + vel[2] * vel[2]);
861 bool moving = speed > 0.1f; 938 bool moving = speed > 0.1f;
@@ -899,6 +976,7 @@ static void do_move(int seq, float frametime) {
899 } 976 }
900 } 977 }
901 978
979bhop_done:
902 bool jump_assist = crouch_jump_apply(cmd, flags, vel); 980 bool jump_assist = crouch_jump_apply(cmd, flags, vel);
903 if (edge_jump_apply(cmd, ent, flags, vel)) 981 if (edge_jump_apply(cmd, ent, flags, vel))
904 jump_assist = true; 982 jump_assist = true;
@@ -975,7 +1053,12 @@ void set_silent_restore(const QAngle &angles) {
975static void hkdFrameStageNotify(void *self, int stage) { 1053static void hkdFrameStageNotify(void *self, int stage) {
976 if (oFrameStageNotify) 1054 if (oFrameStageNotify)
977 oFrameStageNotify(self, stage); 1055 oFrameStageNotify(self, stage);
1056 if (!init_live())
1057 return;
978 if (stage == 5) { 1058 if (stage == 5) {
1059 // FRAME_RENDER_START is the real frame boundary: it lets the paint hooks
1060 // below draw exactly once per frame instead of once per VGUI panel.
1061 overlay_frame_mark();
979 // Skybox texture uploads happen here, not in the VGUI paint hook: the 1062 // Skybox texture uploads happen here, not in the VGUI paint hook: the
980 // material system only downloads procedural textures on a render-safe 1063 // material system only downloads procedural textures on a render-safe
981 // frame stage. 1064 // frame stage.
@@ -1006,14 +1089,15 @@ static bool g_in_draw = false;
1006// our overlay draws are submitted through outer_draw() (menu.cpp), which 1089// our overlay draws are submitted through outer_draw() (menu.cpp), which
1007// wraps them in PushMakeCurrent/PopMakeCurrent with the embedded panel. 1090// wraps them in PushMakeCurrent/PopMakeCurrent with the embedded panel.
1008static void hkdDrawPrintText(void *self, const wchar_t *text, int len, int drawType) { 1091static void hkdDrawPrintText(void *self, const wchar_t *text, int len, int drawType) {
1009 if (g_in_draw) { 1092 if (g_in_draw || !init_live()) {
1010 oDrawPrintText(self, text, len, drawType); 1093 oDrawPrintText(self, text, len, drawType);
1011 return; 1094 return;
1012 } 1095 }
1013 g_in_draw = true; 1096 g_in_draw = true;
1014 oDrawPrintText(self, text, len, drawType); 1097 oDrawPrintText(self, text, len, drawType);
1015 // fallback trigger: fires when the game draws text (menus). Primary draw 1098 // Fallback trigger for frames where nothing paints (menus). The per-frame
1016 // path is the PaintTraverseEx hook (slot 114), which fires every frame. 1099 // budget inside outer_draw() makes this a no-op if PaintTraverseEx already
1100 // drew this frame.
1017 outer_draw(); 1101 outer_draw();
1018 g_in_draw = false; 1102 g_in_draw = false;
1019} 1103}
@@ -1024,7 +1108,7 @@ static void hkdDrawPrintText(void *self, const wchar_t *text, int len, int drawT
1024static void hkdPaintTraverseEx(void *self, long panel, bool paintPopups) { 1108static void hkdPaintTraverseEx(void *self, long panel, bool paintPopups) {
1025 using Fn = void (*)(void *, long, bool); 1109 using Fn = void (*)(void *, long, bool);
1026 ((Fn)oPaintTraverseEx)(self, panel, paintPopups); 1110 ((Fn)oPaintTraverseEx)(self, panel, paintPopups);
1027 if (!g_in_draw) { 1111 if (!g_in_draw && init_live()) {
1028 g_in_draw = true; 1112 g_in_draw = true;
1029 outer_draw(); 1113 outer_draw();
1030 g_in_draw = false; 1114 g_in_draw = false;
@@ -1032,6 +1116,8 @@ static void hkdPaintTraverseEx(void *self, long panel, bool paintPopups) {
1032} 1116}
1033 1117
1034static bool hkdCreateMove(void *self, int seq, float frametime, bool active) { 1118static bool hkdCreateMove(void *self, int seq, float frametime, bool active) {
1119 if (!init_live())
1120 return oCreateMove(self, seq, frametime, active);
1035 bool ret = oCreateMove(self, seq, frametime, active); 1121 bool ret = oCreateMove(self, seq, frametime, active);
1036 do_move(seq, frametime); 1122 do_move(seq, frametime);
1037 1123
@@ -1041,8 +1127,12 @@ static bool hkdCreateMove(void *self, int seq, float frametime, bool active) {
1041 if (!isurface_hooked && g_surface) { 1127 if (!isurface_hooked && g_surface) {
1042 void **svt = VTABLE(g_surface); 1128 void **svt = VTABLE(g_surface);
1043 constexpr size_t VT_SIZE = 256; 1129 constexpr size_t VT_SIZE = 256;
1044 void **newsvt = new void *[VT_SIZE]; 1130 void **newsvt = vtable_clone(g_surface, VT_SIZE);
1045 memcpy(newsvt, svt, VT_SIZE * sizeof(void *)); 1131 if (!newsvt) {
1132 isurface_hooked = 1; // don't retry every tick
1133 logf("ISurface hook: vtable unavailable");
1134 return ret;
1135 }
1046 oDrawPrintText = (DrawPrintTextFn)svt[22]; 1136 oDrawPrintText = (DrawPrintTextFn)svt[22];
1047 newsvt[22] = (void *)&hkdDrawPrintText; 1137 newsvt[22] = (void *)&hkdDrawPrintText;
1048 oPaintTraverseEx = (PaintTraverseExFn)svt[114]; 1138 oPaintTraverseEx = (PaintTraverseExFn)svt[114];
@@ -1059,8 +1149,11 @@ static bool hkdCreateMove(void *self, int seq, float frametime, bool active) {
1059static void install_hook() { 1149static void install_hook() {
1060 void **vt = VTABLE(g_iface.client); 1150 void **vt = VTABLE(g_iface.client);
1061 constexpr size_t SLOTS = 256; 1151 constexpr size_t SLOTS = 256;
1062 void **newvt = new void *[SLOTS]; 1152 void **newvt = vtable_clone(g_iface.client, SLOTS);
1063 memcpy(newvt, vt, SLOTS * sizeof(void *)); 1153 if (!newvt) {
1154 logf("hook install: client vtable unavailable");
1155 return;
1156 }
1064 oCreateMove = (CreateMoveFn)vt[21]; 1157 oCreateMove = (CreateMoveFn)vt[21];
1065 newvt[21] = (void *)&hkdCreateMove; 1158 newvt[21] = (void *)&hkdCreateMove;
1066 oFrameStageNotify = (FrameStageFn)vt[35]; 1159 oFrameStageNotify = (FrameStageFn)vt[35];
@@ -1097,26 +1190,28 @@ static bool is_hl2_process() {
1097static void *setup_thread(void *) { 1190static void *setup_thread(void *) {
1098 for (int i = 0; i < 6000; i++) { 1191 for (int i = 0; i < 6000; i++) {
1099 find_modules(); 1192 find_modules();
1100 if (g_client.valid() && g_engine.valid()) 1193 if (mod_client().valid() && mod_engine().valid())
1101 break; 1194 break;
1102 usleep(200 * 1000); 1195 usleep(200 * 1000);
1103 } 1196 }
1104 if (!g_client.valid() || !g_engine.valid()) { 1197 if (!mod_client().valid() || !mod_engine().valid()) {
1198 init_set_stage(INIT_FAILED);
1105 logf("client.so/engine.so never appeared"); 1199 logf("client.so/engine.so never appeared");
1106 toastf(TOAST_ERROR, "client.so/engine.so not found; hl2hake inactive"); 1200 toastf(TOAST_ERROR, "client.so/engine.so not found; hl2hake inactive");
1107 return nullptr; 1201 return nullptr;
1108 } 1202 }
1109 logf("client.so: %d mapped range(s), %s", (int)g_client.ranges.size(), 1203 logf("client.so: %d mapped range(s), %s", (int)mod_client().ranges.size(),
1110 g_client.path.c_str()); 1204 mod_client().path.c_str());
1111 logf("engine.so: %d mapped range(s), %s", (int)g_engine.ranges.size(), 1205 logf("engine.so: %d mapped range(s), %s", (int)mod_engine().ranges.size(),
1112 g_engine.path.c_str()); 1206 mod_engine().path.c_str());
1113 1207
1114 // resolve factories via dlopen handles 1208 // resolve factories via dlopen handles
1115 void *ch = dlopen(g_client.path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); 1209 void *ch = dlopen(mod_client().path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL);
1116 void *eh = dlopen(g_engine.path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); 1210 void *eh = dlopen(mod_engine().path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL);
1117 auto cf = (CreateInterfaceFn)dlsym(ch, "CreateInterface"); 1211 auto cf = (CreateInterfaceFn)dlsym(ch, "CreateInterface");
1118 auto ef = (CreateInterfaceFn)dlsym(eh, "CreateInterface"); 1212 auto ef = (CreateInterfaceFn)dlsym(eh, "CreateInterface");
1119 if (!cf || !ef) { 1213 if (!cf || !ef) {
1214 init_set_stage(INIT_FAILED);
1120 logf("CreateInterface not found"); 1215 logf("CreateInterface not found");
1121 toastf(TOAST_ERROR, "CreateInterface not found; hl2hake inactive"); 1216 toastf(TOAST_ERROR, "CreateInterface not found; hl2hake inactive");
1122 return nullptr; 1217 return nullptr;
@@ -1134,6 +1229,7 @@ static void *setup_thread(void *) {
1134 usleep(100 * 1000); 1229 usleep(100 * 1000);
1135 } 1230 }
1136 if (!g_iface.client || !g_iface.entitylist || !g_iface.engine) { 1231 if (!g_iface.client || !g_iface.entitylist || !g_iface.engine) {
1232 init_set_stage(INIT_FAILED);
1137 logf("interfaces unresolved: VClient017=%p VClientEntityList003=%p " 1233 logf("interfaces unresolved: VClient017=%p VClientEntityList003=%p "
1138 "VEngineClient014=%p", 1234 "VEngineClient014=%p",
1139 g_iface.client, g_iface.entitylist, g_iface.engine); 1235 g_iface.client, g_iface.entitylist, g_iface.engine);
@@ -1159,10 +1255,10 @@ static void *setup_thread(void *) {
1159 g_iface.cvar = cf(cvar_names[i], nullptr); 1255 g_iface.cvar = cf(cvar_names[i], nullptr);
1160 } 1256 }
1161 if (!g_iface.cvar) { 1257 if (!g_iface.cvar) {
1162 size_t slash = g_engine.path.find_last_of('/'); 1258 size_t slash = mod_engine().path.find_last_of('/');
1163 if (slash != std::string::npos) { 1259 if (slash != std::string::npos) {
1164 std::string path = 1260 std::string path =
1165 g_engine.path.substr(0, slash + 1) + "libvstdlib.so"; 1261 mod_engine().path.substr(0, slash + 1) + "libvstdlib.so";
1166 void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); 1262 void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL);
1167 if (!h) 1263 if (!h)
1168 h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL); 1264 h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL);
@@ -1195,15 +1291,23 @@ static void *setup_thread(void *) {
1195 toastf(TOAST_WARN, "Debug overlay missing; ESP projection uses fallback"); 1291 toastf(TOAST_WARN, "Debug overlay missing; ESP projection uses fallback");
1196 if (!g_iface.enginetrace) 1292 if (!g_iface.enginetrace)
1197 toastf(TOAST_WARN, "Engine trace missing; vischeck fails open"); 1293 toastf(TOAST_WARN, "Engine trace missing; vischeck fails open");
1294 init_set_stage(INIT_INTERFACES);
1295
1296 init_conmsg();
1297 // Config load and ISurface/font discovery must finish before any hook is
1298 // allowed past the init_live() gate: the game thread keeps calling
1299 // CreateMove/PaintTraverse while this thread runs.
1300 install_menu();
1301 init_set_stage(INIT_READY);
1198 1302
1199 if (!find_input_global()) { 1303 if (!find_input_global()) {
1304 init_set_stage(INIT_FAILED);
1200 toastf(TOAST_ERROR, "Input hook failed; movement features disabled"); 1305 toastf(TOAST_ERROR, "Input hook failed; movement features disabled");
1201 return nullptr; 1306 return nullptr;
1202 } 1307 }
1203
1204 init_conmsg();
1205 install_hook(); 1308 install_hook();
1206 install_menu(); 1309 init_set_stage(INIT_LIVE);
1310 logf("init: live (surface %p)", (void *)g_surface);
1207 if (g_surface) 1311 if (g_surface)
1208 toastf(TOAST_INFO, "hl2hake loaded - Insert opens the menu"); 1312 toastf(TOAST_INFO, "hl2hake loaded - Insert opens the menu");
1209 return nullptr; 1313 return nullptr;
diff --git a/src/hl2h.h b/src/hl2h.h
index 86573d8..f1a1ea0 100644
--- a/src/hl2h.h
+++ b/src/hl2h.h
@@ -11,6 +11,8 @@
11#include <pthread.h> 11#include <pthread.h>
12#include <unistd.h> 12#include <unistd.h>
13 13
14#include "overlay.h"
15
14// Shared float-precision constant. More digits than a float can hold are 16// Shared float-precision constant. More digits than a float can hold are
15// rounded at compile time and have no runtime cost; a coarse value would only 17// rounded at compile time and have no runtime cost; a coarse value would only
16// hurt angular accuracy, never performance. 18// hurt angular accuracy, never performance.
@@ -24,6 +26,31 @@ void logf(const char *fmt, ...);
24void logd(const char *fmt, ...); // debug-only (HL2H_DEBUG=1) 26void logd(const char *fmt, ...); // debug-only (HL2H_DEBUG=1)
25 27
26// --------------------------------------------------------------------------- 28// ---------------------------------------------------------------------------
29// init readiness
30// ---------------------------------------------------------------------------
31//
32// setup_thread runs detached while the game threads are already live, and some
33// hooks are installed lazily from the game thread. Hooks therefore always stay
34// installed but pass straight through to the original until setup reaches
35// INIT_LIVE, so the game can never observe half-initialised state. INIT_FAILED
36// is terminal: the cheat stays inert instead of half-working.
37enum {
38 INIT_FAILED = -1,
39 INIT_NONE = 0,
40 INIT_INTERFACES = 1, // modules + interfaces + netvars resolved
41 INIT_READY = 2, // config loaded, ISurface/fonts resolved
42 INIT_LIVE = 3 // hooks may run their bodies
43};
44void init_set_stage(int stage);
45int init_stage(); // acquire load
46inline bool init_live() { return init_stage() == INIT_LIVE; }
47
48// Clone a vtable into a new array, preserving the Itanium ABI header at
49// [-2]/[-1] (offset-to-top and typeinfo) so RTTI on the hooked object still
50// works. Returns the slot-0 pointer to install (i.e. the clone + 2).
51void **vtable_clone(void *iface, size_t slots);
52
53// ---------------------------------------------------------------------------
27// on-screen toasts 54// on-screen toasts
28// --------------------------------------------------------------------------- 55// ---------------------------------------------------------------------------
29 56
@@ -51,7 +78,17 @@ struct Module {
51 return false; 78 return false;
52 } 79 }
53}; 80};
54extern Module g_client, g_engine, g_server; 81// Immutable module snapshots. A reader gets a reference that stays valid for
82// the whole process: find_modules() publishes a new snapshot instead of
83// mutating the old one, and retired snapshots are intentionally never freed, so
84// a concurrent refresh can never invalidate a reference already in use.
85const Module &mod_client();
86const Module &mod_engine();
87const Module &mod_server();
88
89// True only when the whole byte range [p, p+n) lies inside one known module
90// range. Use before dereferencing pointers read out of engine memory.
91bool module_range(uintptr_t p, size_t n);
55 92
56// --------------------------------------------------------------------------- 93// ---------------------------------------------------------------------------
57// game interfaces 94// game interfaces
@@ -308,6 +345,9 @@ void skybox_invalidate();
308void skybox_reload_image(); 345void skybox_reload_image();
309bool resolve_server_netvars(); 346bool resolve_server_netvars();
310int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out); 347int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out);
348// Authoritative server velocity; client-side m_vecVelocity is not networked for
349// NPCs. Returns false when the server interface or datamap field is missing.
350bool server_entity_velocity(int idx, Vector *out);
311bool server_set_velocity_z(int idx, float vz); 351bool server_set_velocity_z(int idx, float vz);
312int server_active_weapon(int idx); 352int server_active_weapon(int idx);
313int server_npc_relation(int idx, int local_idx); 353int server_npc_relation(int idx, int local_idx);
@@ -332,7 +372,8 @@ void find_modules();
332bool module_jump_slot(const char *path, const char *name, uintptr_t *vaddr, 372bool module_jump_slot(const char *path, const char *name, uintptr_t *vaddr,
333 uintptr_t *bias); 373 uintptr_t *bias);
334void init_conmsg(); 374void init_conmsg();
335bool feature_active(int feature); 375bool feature_active(int feature); // enabled AND bind held (or unbound)
376bool feature_enabled(int feature); // enabled, regardless of bind
336bool feature_has_bind(int feature); 377bool feature_has_bind(int feature);
337bool feature_bind_held(int feature); 378bool feature_bind_held(int feature);
338bool esp_option(int opt); 379bool esp_option(int opt);
@@ -359,6 +400,8 @@ int engine_get_local_player();
359void draw_text_col(int x, int y, const char *s, int r, int g, int b); 400void draw_text_col(int x, int y, const char *s, int r, int g, int b);
360void draw_text_small_col(int x, int y, const char *s, int r, int g, int b); 401void draw_text_small_col(int x, int y, const char *s, int r, int g, int b);
361void esp_draw(); 402void esp_draw();
403void esp_draw_fov_circle(); // aimbot cone, independent of the ESP toggle
404void esp_reset_session(); // map change / respawn: clear per-map latches
362void esp_draw_items(); 405void esp_draw_items();
363void esp_draw_projectiles(); 406void esp_draw_projectiles();
364void prop_tint_update(); 407void prop_tint_update();
@@ -395,7 +438,4 @@ bool gui_bind_capture_active();
395QAngle gui_frozen_view(); 438QAngle gui_frozen_view();
396void gui_config_init(); 439void gui_config_init();
397void gui_config_write(FILE *f); 440void gui_config_write(FILE *f);
398// ISurface pointer (defined in menu.cpp, used for drawing tests)
399struct ISurface;
400extern ISurface *g_surface;
401#endif 441#endif
diff --git a/src/menu.cpp b/src/menu.cpp
index 1064365..f3a9bb9 100644
--- a/src/menu.cpp
+++ b/src/menu.cpp
@@ -19,10 +19,10 @@ void init_conmsg() {
19 if (g_conmsg) 19 if (g_conmsg)
20 return; 20 return;
21 // fallback: dlopen libtier0.so from the engine directory 21 // fallback: dlopen libtier0.so from the engine directory
22 size_t slash = g_engine.path.find_last_of('/'); 22 size_t slash = mod_engine().path.find_last_of('/');
23 if (slash == std::string::npos) 23 if (slash == std::string::npos)
24 return; 24 return;
25 std::string tier0 = g_engine.path.substr(0, slash + 1) + "libtier0.so"; 25 std::string tier0 = mod_engine().path.substr(0, slash + 1) + "libtier0.so";
26 void *h = dlopen(tier0.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); 26 void *h = dlopen(tier0.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL);
27 if (!h) 27 if (!h)
28 h = dlopen(tier0.c_str(), RTLD_LAZY | RTLD_GLOBAL); 28 h = dlopen(tier0.c_str(), RTLD_LAZY | RTLD_GLOBAL);
@@ -640,6 +640,11 @@ static bool config_load() {
640 g_esp_col_friendly[key[1] == 'r' ? 0 : key[1] == 'g' ? 1 : 2] = v; 640 g_esp_col_friendly[key[1] == 'r' ? 0 : key[1] == 'g' ? 1 : 2] = v;
641 } 641 }
642 } 642 }
643 // Scalar-only sections ([view], [movement], [chams_*]) intentionally
644 // leave cur == nullptr. A stray active=/bind= line in one of them must
645 // not be dereferenced (hand-edited or corrupt config).
646 if (!cur)
647 continue;
643 if (strcmp(key, "active") == 0) 648 if (strcmp(key, "active") == 0)
644 cur->active = atoi(val) != 0; 649 cur->active = atoi(val) != 0;
645 else if (strcmp(key, "bind") == 0) 650 else if (strcmp(key, "bind") == 0)
@@ -867,6 +872,11 @@ bool feature_active(int feature) {
867 return f->bind == 0 || key_held(f->bind); 872 return f->bind == 0 || key_held(f->bind);
868} 873}
869 874
875bool feature_enabled(int feature) {
876 FeatureState *f = feature_state(feature);
877 return f && f->active;
878}
879
870bool feature_has_bind(int feature) { 880bool feature_has_bind(int feature) {
871 FeatureState *f = feature_state(feature); 881 FeatureState *f = feature_state(feature);
872 return f && f->bind != 0; 882 return f && f->bind != 0;
@@ -924,81 +934,39 @@ bool aimbot_fov_circle() { return g_aim_fov_circle; }
924struct ISurface; 934struct ISurface;
925using HFont = unsigned long; 935using HFont = unsigned long;
926 936
927ISurface *g_surface = nullptr;
928static HFont g_font = 0; 937static HFont g_font = 0;
929static HFont g_font_small = 0; 938static HFont g_font_small = 0;
930 939
931// VGUI_Surface030 vtable slots (confirmed via binary disassembly of vguimatsurface.so): 940// ISurface access lives in overlay.cpp; these wrappers keep the call sites in
932// 8 = PushMakeCurrent(VPANEL, bool useInsets) — reads args at 0xc/0x10(%ebp) 941// this file unchanged.
933// 9 = PopMakeCurrent(VPANEL) — pops the make-current stack at this+0x264/0x270 942static int surf_get_embedded_panel() { return overlay_embedded_panel(); }
934// 10 = DrawSetColor(int r, int g, int b, int a) — stores rgba at this+0x24
935// 12 = DrawFilledRect(int x0, int y0, int x1, int y1) — alpha check at this+0x27
936// 17 = DrawSetTextFont(HFont), 18 = DrawSetTextColor(int r,g,b,a)
937// 20 = DrawSetTextPos(int x, int y), 22 = DrawPrintText(wchar_t*, len, drawType)
938// 66 = CreateFont(), 67 = AddGlyphSetToFont(font, name, tall, weight,
939// blur, scanlines, flags, rangeMin, rangeMax)
940static constexpr int SURF_GetEmbeddedPanel = 6;
941static constexpr int SURF_PushMakeCurrent = 8;
942static constexpr int SURF_PopMakeCurrent = 9;
943static constexpr int SURF_DrawSetColor = 10;
944static constexpr int SURF_DrawFilledRect = 12;
945static constexpr int SURF_DrawSetTextFont = 17;
946static constexpr int SURF_DrawSetTextColor = 18;
947static constexpr int SURF_DrawSetTextPos = 20;
948static constexpr int SURF_DrawPrintText = 22;
949static constexpr int SURF_CreateFont = 66;
950static constexpr int SURF_AddGlyphSetToFont = 67;
951
952static int surf_get_embedded_panel() {
953 using Fn = int (*)(void *);
954 return vfunc<Fn>(g_surface, SURF_GetEmbeddedPanel)(g_surface);
955}
956static void surf_push_make_current(int panel, bool use_insets) { 943static void surf_push_make_current(int panel, bool use_insets) {
957 using Fn = void (*)(void *, int, bool); 944 overlay_push_make_current(panel, use_insets);
958 vfunc<Fn>(g_surface, SURF_PushMakeCurrent)(g_surface, panel, use_insets);
959} 945}
960static void surf_pop_make_current(int panel) { 946static void surf_pop_make_current(int panel) {
961 using Fn = void (*)(void *, int); 947 overlay_pop_make_current(panel);
962 vfunc<Fn>(g_surface, SURF_PopMakeCurrent)(g_surface, panel);
963} 948}
964static void surf_draw_set_color(int r, int g, int b, int a) { 949static void surf_draw_set_color(int r, int g, int b, int a) {
965 using Fn = void (*)(void *, int, int, int, int); 950 overlay_color(r, g, b, a);
966 vfunc<Fn>(g_surface, SURF_DrawSetColor)(g_surface, r, g, b, a);
967} 951}
968static void surf_draw_filled_rect(int x0, int y0, int x1, int y1) { 952static void surf_draw_filled_rect(int x0, int y0, int x1, int y1) {
969 using Fn = void (*)(void *, int, int, int, int); 953 overlay_fill(x0, y0, x1, y1);
970 vfunc<Fn>(g_surface, SURF_DrawFilledRect)(g_surface, x0, y0, x1, y1);
971} 954}
972static void surf_draw_set_text_color(int r, int g, int b, int a) { 955static void surf_draw_set_text_color(int r, int g, int b, int a) {
973 using Fn = void (*)(void *, int, int, int, int); 956 overlay_text_color(r, g, b, a);
974 vfunc<Fn>(g_surface, SURF_DrawSetTextColor)(g_surface, r, g, b, a);
975}
976static void surf_draw_set_text_font(HFont f) {
977 using Fn = void (*)(void *, HFont);
978 vfunc<Fn>(g_surface, SURF_DrawSetTextFont)(g_surface, f);
979}
980static void surf_draw_set_text_pos(int x, int y) {
981 using Fn = void (*)(void *, int, int);
982 vfunc<Fn>(g_surface, SURF_DrawSetTextPos)(g_surface, x, y);
983} 957}
958static void surf_draw_set_text_font(HFont f) { overlay_text_font(f); }
959static void surf_draw_set_text_pos(int x, int y) { overlay_text_pos(x, y); }
984static void surf_draw_print_text(const wchar_t *text, int count) { 960static void surf_draw_print_text(const wchar_t *text, int count) {
985 using Fn = void (*)(void *, const wchar_t *, int, int); 961 overlay_print_text(text, count);
986 vfunc<Fn>(g_surface, SURF_DrawPrintText)(g_surface, text, count, 0); // 0 = FONT_DRAW_DEFAULT
987}
988static HFont surf_create_font() {
989 using Fn = HFont (*)(void *);
990 return vfunc<Fn>(g_surface, SURF_CreateFont)(g_surface);
991} 962}
963static HFont surf_create_font() { return overlay_create_font(); }
992static bool surf_set_font_glyph_set(HFont f, const char *name, int tall, 964static bool surf_set_font_glyph_set(HFont f, const char *name, int tall,
993 int weight, int blur, int scanlines, 965 int weight, int blur, int scanlines,
994 int flags, int range_min = 0, 966 int flags, int range_min = 0,
995 int range_max = 0) { 967 int range_max = 0) {
996 using Fn = bool (*)(void *, HFont, const char *, int, int, int, int, int, int, 968 return overlay_add_glyph_set(f, name, tall, weight, blur, scanlines, flags,
997 int); 969 range_min, range_max);
998 return vfunc<Fn>(g_surface, SURF_AddGlyphSetToFont)(g_surface, f, name, tall,
999 weight, blur, scanlines,
1000 flags, range_min,
1001 range_max);
1002} 970}
1003 971
1004// wide string conversion for DrawPrintText 972// wide string conversion for DrawPrintText
@@ -1071,6 +1039,11 @@ void fullbright_update() {
1071// (see hkdDrawPrintText hook in hl2h.cpp): we wrap our draws in 1039// (see hkdDrawPrintText hook in hl2h.cpp): we wrap our draws in
1072// PushMakeCurrent/PopMakeCurrent so they are submitted with a clean clip. 1040// PushMakeCurrent/PopMakeCurrent so they are submitted with a clean clip.
1073void outer_draw() { 1041void outer_draw() {
1042 // PaintTraverseEx runs for every VGUI panel and DrawPrintText for every text
1043 // draw, so collapse them to one draw per engine frame (see overlay.h).
1044 if (!overlay_should_draw())
1045 return;
1046
1074 poll_keyboard(); 1047 poll_keyboard();
1075 fullbright_update(); 1048 fullbright_update();
1076 world_tint_update(); 1049 world_tint_update();
@@ -1083,10 +1056,16 @@ void outer_draw() {
1083 bool esp_on = feature_active(FEAT_ESP); 1056 bool esp_on = feature_active(FEAT_ESP);
1084 bool items_on = feature_active(FEAT_ITEM_ESP); 1057 bool items_on = feature_active(FEAT_ITEM_ESP);
1085 bool projs_on = feature_active(FEAT_PROJECTILES); 1058 bool projs_on = feature_active(FEAT_PROJECTILES);
1059 // The FOV circle belongs to the aim features, not to ESP, and it is a visual
1060 // aid: use the enable toggle rather than the bind, otherwise it only appears
1061 // while the aim key is held (default bind is mouse5).
1062 bool aim_on = (feature_enabled(FEAT_AIMBOT) ||
1063 feature_enabled(FEAT_SILENTAIM)) &&
1064 aimbot_fov_circle();
1086 bool toasts = gui_toasts_active(); 1065 bool toasts = gui_toasts_active();
1087 if (!g_surface || 1066 if (!g_surface ||
1088 (!gui_is_open() && !draw_test && !esp_on && !items_on && !projs_on && 1067 (!gui_is_open() && !draw_test && !esp_on && !items_on && !projs_on &&
1089 !toasts)) 1068 !aim_on && !toasts))
1090 return; 1069 return;
1091 1070
1092 // Level/menu -> game transitions can invalidate the VGUI font handle. 1071 // Level/menu -> game transitions can invalidate the VGUI font handle.
@@ -1147,6 +1126,8 @@ void outer_draw() {
1147 if (!panel) 1126 if (!panel)
1148 return; 1127 return;
1149 surf_push_make_current(panel, false); 1128 surf_push_make_current(panel, false);
1129 if (aim_on && overlay_font)
1130 esp_draw_fov_circle();
1150 if (esp_on && overlay_font) 1131 if (esp_on && overlay_font)
1151 esp_draw(); 1132 esp_draw();
1152 if (items_on && overlay_font) 1133 if (items_on && overlay_font)
diff --git a/src/modules.cpp b/src/modules.cpp
index 7a82028..a671d82 100644
--- a/src/modules.cpp
+++ b/src/modules.cpp
@@ -1,10 +1,27 @@
1// modules.cpp — /proc/self/maps parsing and module discovery 1// modules.cpp — /proc/self/maps parsing and module discovery
2#include "hl2h.h" 2#include "hl2h.h"
3#include <atomic>
3#include <elf.h> 4#include <elf.h>
4#include <mutex>
5#include <vector> 5#include <vector>
6 6
7static std::mutex g_modules_mutex; 7// Immutable snapshots. Readers load a pointer with acquire semantics and get a
8// module that never changes underneath them. find_modules() publishes a fresh
9// copy instead of mutating the current one; retired snapshots are deliberately
10// leaked so a reference already handed to another thread stays valid.
11static const Module g_no_module;
12static std::atomic<const Module *> g_client_snap{&g_no_module};
13static std::atomic<const Module *> g_engine_snap{&g_no_module};
14static std::atomic<const Module *> g_server_snap{&g_no_module};
15
16const Module &mod_client() {
17 return *g_client_snap.load(std::memory_order_acquire);
18}
19const Module &mod_engine() {
20 return *g_engine_snap.load(std::memory_order_acquire);
21}
22const Module &mod_server() {
23 return *g_server_snap.load(std::memory_order_acquire);
24}
8 25
9static bool basename_is(const char *path, const char *base) { 26static bool basename_is(const char *path, const char *base) {
10 size_t pl = strlen(path), bl = strlen(base); 27 size_t pl = strlen(path), bl = strlen(base);
@@ -55,6 +72,9 @@ void find_modules() {
55 else if (engine.valid() && engine.ranges.back().file && 72 else if (engine.valid() && engine.ranges.back().file &&
56 lo == engine.ranges.back().hi && hi - lo <= 0x1000000) 73 lo == engine.ranges.back().hi && hi - lo <= 0x1000000)
57 m = &engine; 74 m = &engine;
75 else if (server.valid() && server.ranges.back().file &&
76 lo == server.ranges.back().hi && hi - lo <= 0x1000000)
77 m = &server;
58 } 78 }
59 if (!m) 79 if (!m)
60 continue; 80 continue;
@@ -72,16 +92,25 @@ void find_modules() {
72 } 92 }
73 } 93 }
74 fclose(f); 94 fclose(f);
75 // setup_thread and resolve_server_netvars can both call this; the old code 95 // setup_thread and resolve_server_netvars can both call this. Publish whole
76 // reset the shared Module globals and reallocated their vectors live, which 96 // snapshots atomically so a reader never iterates a vector being reallocated.
77 // crashed under the startup thread. Publish complete snapshots instead.
78 std::lock_guard<std::mutex> lock(g_modules_mutex);
79 if (client.valid()) 97 if (client.valid())
80 g_client = std::move(client); 98 g_client_snap.store(new Module(std::move(client)), std::memory_order_release);
81 if (engine.valid()) 99 if (engine.valid())
82 g_engine = std::move(engine); 100 g_engine_snap.store(new Module(std::move(engine)), std::memory_order_release);
83 if (server.valid()) 101 if (server.valid())
84 g_server = std::move(server); 102 g_server_snap.store(new Module(std::move(server)), std::memory_order_release);
103}
104
105bool module_range(uintptr_t p, size_t n) {
106 if (n == 0)
107 return true;
108 uintptr_t last = p + (n - 1);
109 if (last < p)
110 return false; // address-space wrap
111 return (mod_client().contains(p) && mod_client().contains(last)) ||
112 (mod_engine().contains(p) && mod_engine().contains(last)) ||
113 (mod_server().contains(p) && mod_server().contains(last));
85} 114}
86 115
87// --------------------------------------------------------------------------- 116// ---------------------------------------------------------------------------
diff --git a/src/netvars.cpp b/src/netvars.cpp
index e22ff38..8e9579c 100644
--- a/src/netvars.cpp
+++ b/src/netvars.cpp
@@ -6,11 +6,15 @@
6// ClientClass (2004-era): CreateFn@0, CreateEventFn@4, Name@8, RecvTable@0xC, Next@0x10 6// ClientClass (2004-era): CreateFn@0, CreateEventFn@4, Name@8, RecvTable@0xC, Next@0x10
7 7
8static bool module_string(uintptr_t p, char *out, size_t outn) { 8static bool module_string(uintptr_t p, char *out, size_t outn) {
9 if (!g_client.contains(p) && !g_engine.contains(p) && !g_server.contains(p)) 9 if (!p || outn == 0 || !module_range(p, 1))
10 return false; 10 return false;
11 const char *s = (const char *)p; 11 const char *s = (const char *)p;
12 size_t i = 0; 12 size_t i = 0;
13 while (i < outn - 1) { 13 while (i < outn - 1) {
14 // Validate every byte, not just the first: a string near the end of a
15 // mapping must not walk into an unmapped page looking for its NUL.
16 if (!module_range(p + i, 1))
17 return false;
14 unsigned char ch = (unsigned char)s[i]; 18 unsigned char ch = (unsigned char)s[i];
15 if (ch == 0) 19 if (ch == 0)
16 break; 20 break;
@@ -30,9 +34,13 @@ static int g_dumped_tables = 0;
30static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { 34static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
31 if (depth > 8) 35 if (depth > 8)
32 return; 36 return;
37 // RecvTable is 16 bytes; validate before the first read (the table pointer
38 // and the nested pDataTable both come from engine memory).
39 if (!module_range(table, 0x10))
40 return;
33 uintptr_t props = *(uintptr_t *)(table + 0x00); 41 uintptr_t props = *(uintptr_t *)(table + 0x00);
34 int32_t nprops = *(int32_t *)(table + 0x04); 42 int32_t nprops = *(int32_t *)(table + 0x04);
35 if (!props || !g_client.contains(props) || nprops <= 0 || nprops > 512) 43 if (!props || !mod_client().contains(props) || nprops <= 0 || nprops > 512)
36 return; 44 return;
37 45
38 char tname[64]; 46 char tname[64];
@@ -52,7 +60,7 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
52 60
53 for (int32_t i = 0; i < nprops; i++) { 61 for (int32_t i = 0; i < nprops; i++) {
54 uintptr_t prop = props + (size_t)i * 0x3C; 62 uintptr_t prop = props + (size_t)i * 0x3C;
55 if (!g_client.contains(prop)) 63 if (!mod_client().contains(prop))
56 return; 64 return;
57 uintptr_t pname = *(uintptr_t *)(prop + 0x00); 65 uintptr_t pname = *(uintptr_t *)(prop + 0x00);
58 int32_t ptype = *(int32_t *)(prop + 0x04); 66 int32_t ptype = *(int32_t *)(prop + 0x04);
@@ -74,7 +82,7 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
74 strncmp(pname2, "m_hMyWeapons", 12) == 0) { 82 strncmp(pname2, "m_hMyWeapons", 12) == 0) {
75 int off = (int)(base_off + (uint32_t)poff); 83 int off = (int)(base_off + (uint32_t)poff);
76 uintptr_t elem = parray ? parray : ptable; 84 uintptr_t elem = parray ? parray : ptable;
77 if (elem) { 85 if (elem && module_range(elem, 0x30)) {
78 int32_t eoff = *(int32_t *)(elem + 0x2C); 86 int32_t eoff = *(int32_t *)(elem + 0x2C);
79 if (eoff) 87 if (eoff)
80 off = (int)(base_off + (uint32_t)eoff); 88 off = (int)(base_off + (uint32_t)eoff);
@@ -89,7 +97,7 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
89 if (g_net.ammo < 0 && strncmp(pname2, "m_iAmmo", 7) == 0) { 97 if (g_net.ammo < 0 && strncmp(pname2, "m_iAmmo", 7) == 0) {
90 int off = (int)(base_off + (uint32_t)poff); 98 int off = (int)(base_off + (uint32_t)poff);
91 uintptr_t elem = parray ? parray : ptable; 99 uintptr_t elem = parray ? parray : ptable;
92 if (elem) { 100 if (elem && module_range(elem, 0x30)) {
93 int32_t eoff = *(int32_t *)(elem + 0x2C); 101 int32_t eoff = *(int32_t *)(elem + 0x2C);
94 if (eoff) 102 if (eoff)
95 off = (int)(base_off + (uint32_t)eoff); 103 off = (int)(base_off + (uint32_t)eoff);
@@ -113,7 +121,7 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
113 } else if (strcmp(pname2, "m_fFlags") == 0 && g_net.flags < 0) { 121 } else if (strcmp(pname2, "m_fFlags") == 0 && g_net.flags < 0) {
114 g_net.flags = (int)(base_off + (uint32_t)poff); 122 g_net.flags = (int)(base_off + (uint32_t)poff);
115 } else if (strcmp(pname2, "m_vecVelocity") == 0 && g_net.velocity < 0) { 123 } else if (strcmp(pname2, "m_vecVelocity") == 0 && g_net.velocity < 0) {
116 if (parray && g_client.contains(parray)) { 124 if (parray && module_range(parray, 0x30)) {
117 int32_t eoff = *(int32_t *)(parray + 0x2C); 125 int32_t eoff = *(int32_t *)(parray + 0x2C);
118 g_net.velocity = (int)(base_off + (uint32_t)(eoff ? eoff : poff)); 126 g_net.velocity = (int)(base_off + (uint32_t)(eoff ? eoff : poff));
119 } else { 127 } else {
@@ -126,6 +134,16 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
126 } else if (strcmp(pname2, "m_hActiveWeapon") == 0 && 134 } else if (strcmp(pname2, "m_hActiveWeapon") == 0 &&
127 g_net.activeweapon < 0) { 135 g_net.activeweapon < 0) {
128 g_net.activeweapon = (int)(base_off + (uint32_t)poff); 136 g_net.activeweapon = (int)(base_off + (uint32_t)poff);
137 } else if (strcmp(pname2, "m_hOwnerEntity") == 0 && g_net.owner < 0) {
138 // Used by vischeck to ignore a target's held weapon / bone followers,
139 // and by item ESP for weapon ownership.
140 int off = (int)(base_off + (uint32_t)poff);
141 if (off >= 0x20 && off <= 0x2000)
142 g_net.owner = off;
143 } else if (strcmp(pname2, "m_hOwner") == 0 && g_net.weapon_owner < 0) {
144 int off = (int)(base_off + (uint32_t)poff);
145 if (off >= 0x20 && off <= 0x2000)
146 g_net.weapon_owner = off;
129 } else if (strcmp(pname2, "m_clrRender") == 0 && g_net.clr_render < 0) { 147 } else if (strcmp(pname2, "m_clrRender") == 0 && g_net.clr_render < 0) {
130 int off = (int)(base_off + (uint32_t)poff); 148 int off = (int)(base_off + (uint32_t)poff);
131 // Reject the 0 offset some embedded tables report; the real render 149 // Reject the 0 offset some embedded tables report; the real render
@@ -149,7 +167,7 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
149 } else if (strcmp(pname2, "m_iTeamNum") == 0 && g_net.teamnum < 0) { 167 } else if (strcmp(pname2, "m_iTeamNum") == 0 && g_net.teamnum < 0) {
150 g_net.teamnum = (int)(base_off + (uint32_t)poff); 168 g_net.teamnum = (int)(base_off + (uint32_t)poff);
151 } else if (strcmp(pname2, "m_vecOrigin") == 0 && g_net.origin < 0) { 169 } else if (strcmp(pname2, "m_vecOrigin") == 0 && g_net.origin < 0) {
152 if (parray && g_client.contains(parray)) { 170 if (parray && module_range(parray, 0x30)) {
153 int32_t eoff = *(int32_t *)(parray + 0x2C); 171 int32_t eoff = *(int32_t *)(parray + 0x2C);
154 g_net.origin = (int)(base_off + (uint32_t)(eoff ? eoff : poff)); 172 g_net.origin = (int)(base_off + (uint32_t)(eoff ? eoff : poff));
155 } else { 173 } else {
@@ -157,21 +175,21 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) {
157 } 175 }
158 } else if (strncmp(pname2, "m_vecViewOffset", 15) == 0 && 176 } else if (strncmp(pname2, "m_vecViewOffset", 15) == 0 &&
159 g_net.viewoffset < 0) { 177 g_net.viewoffset < 0) {
160 if (parray && g_client.contains(parray)) { 178 if (parray && module_range(parray, 0x30)) {
161 int32_t eoff = *(int32_t *)(parray + 0x2C); 179 int32_t eoff = *(int32_t *)(parray + 0x2C);
162 g_net.viewoffset = (int)(base_off + (uint32_t)(eoff ? eoff : poff)); 180 g_net.viewoffset = (int)(base_off + (uint32_t)(eoff ? eoff : poff));
163 } else { 181 } else {
164 g_net.viewoffset = (int)(base_off + (uint32_t)poff); 182 g_net.viewoffset = (int)(base_off + (uint32_t)poff);
165 } 183 }
166 } else if (strcmp(pname2, "m_vecMins") == 0 && g_net.mins < 0) { 184 } else if (strcmp(pname2, "m_vecMins") == 0 && g_net.mins < 0) {
167 if (parray && g_client.contains(parray)) { 185 if (parray && module_range(parray, 0x30)) {
168 int32_t eoff = *(int32_t *)(parray + 0x2C); 186 int32_t eoff = *(int32_t *)(parray + 0x2C);
169 g_net.mins = (int)(base_off + (uint32_t)(eoff ? eoff : poff)); 187 g_net.mins = (int)(base_off + (uint32_t)(eoff ? eoff : poff));
170 } else { 188 } else {
171 g_net.mins = (int)(base_off + (uint32_t)poff); 189 g_net.mins = (int)(base_off + (uint32_t)poff);
172 } 190 }
173 } else if (strcmp(pname2, "m_vecMaxs") == 0 && g_net.maxs < 0) { 191 } else if (strcmp(pname2, "m_vecMaxs") == 0 && g_net.maxs < 0) {
174 if (parray && g_client.contains(parray)) { 192 if (parray && module_range(parray, 0x30)) {
175 int32_t eoff = *(int32_t *)(parray + 0x2C); 193 int32_t eoff = *(int32_t *)(parray + 0x2C);
176 g_net.maxs = (int)(base_off + (uint32_t)(eoff ? eoff : poff)); 194 g_net.maxs = (int)(base_off + (uint32_t)(eoff ? eoff : poff));
177 } else { 195 } else {
@@ -187,7 +205,7 @@ static bool resolve_movetype() {
187 const size_t nlen = sizeof(needle); 205 const size_t nlen = sizeof(needle);
188 206
189 uintptr_t str_va = 0; 207 uintptr_t str_va = 0;
190 for (auto &r : g_client.ranges) { 208 for (auto &r : mod_client().ranges) {
191 const uint8_t *base = (const uint8_t *)r.lo; 209 const uint8_t *base = (const uint8_t *)r.lo;
192 size_t span = r.hi - r.lo; 210 size_t span = r.hi - r.lo;
193 for (size_t i = 0; i + nlen <= span; i++) { 211 for (size_t i = 0; i + nlen <= span; i++) {
@@ -208,7 +226,7 @@ static bool resolve_movetype() {
208 int counts[16] = {}; 226 int counts[16] = {};
209 int ncand = 0; 227 int ncand = 0;
210 const uint32_t target = (uint32_t)str_va; 228 const uint32_t target = (uint32_t)str_va;
211 for (auto &r : g_client.ranges) { 229 for (auto &r : mod_client().ranges) {
212 const uint8_t *base = (const uint8_t *)r.lo; 230 const uint8_t *base = (const uint8_t *)r.lo;
213 size_t span = r.hi - r.lo; 231 size_t span = r.hi - r.lo;
214 for (size_t off = 4; off + 8 <= span; off += 4) { 232 for (size_t off = 4; off + 8 <= span; off += 4) {
@@ -247,7 +265,7 @@ static bool resolve_absvelocity() {
247 const size_t nlen = sizeof(needle); 265 const size_t nlen = sizeof(needle);
248 266
249 uintptr_t str_va = 0; 267 uintptr_t str_va = 0;
250 for (auto &r : g_client.ranges) { 268 for (auto &r : mod_client().ranges) {
251 const uint8_t *base = (const uint8_t *)r.lo; 269 const uint8_t *base = (const uint8_t *)r.lo;
252 size_t span = r.hi - r.lo; 270 size_t span = r.hi - r.lo;
253 for (size_t i = 0; i + nlen <= span; i++) { 271 for (size_t i = 0; i + nlen <= span; i++) {
@@ -268,7 +286,7 @@ static bool resolve_absvelocity() {
268 int counts[16] = {}; 286 int counts[16] = {};
269 int ncand = 0; 287 int ncand = 0;
270 const uint32_t target = (uint32_t)str_va; 288 const uint32_t target = (uint32_t)str_va;
271 for (auto &r : g_client.ranges) { 289 for (auto &r : mod_client().ranges) {
272 const uint8_t *base = (const uint8_t *)r.lo; 290 const uint8_t *base = (const uint8_t *)r.lo;
273 size_t span = r.hi - r.lo; 291 size_t span = r.hi - r.lo;
274 for (size_t off = 4; off + 8 <= span; off += 4) { 292 for (size_t off = 4; off + 8 <= span; off += 4) {
@@ -306,12 +324,12 @@ void resolve_netvars() {
306 using GetAllClassesFn = void *(*)(void *); 324 using GetAllClassesFn = void *(*)(void *);
307 uintptr_t head = 325 uintptr_t head =
308 (uintptr_t)vfunc<GetAllClassesFn>(g_iface.client, 8)(g_iface.client); 326 (uintptr_t)vfunc<GetAllClassesFn>(g_iface.client, 8)(g_iface.client);
309 if (!g_client.contains(head)) { 327 if (!mod_client().contains(head)) {
310 logf("GetAllClasses returned %p, outside client module", (void *)head); 328 logf("GetAllClasses returned %p, outside client module", (void *)head);
311 return; 329 return;
312 } 330 }
313 int classes = 0; 331 int classes = 0;
314 for (uintptr_t cc = head; cc && g_client.contains(cc) && classes < 1024; 332 for (uintptr_t cc = head; cc && mod_client().contains(cc) && classes < 1024;
315 cc = *(uintptr_t *)(cc + 0x10)) { 333 cc = *(uintptr_t *)(cc + 0x10)) {
316 classes++; 334 classes++;
317 collect_netvars(*(uintptr_t *)(cc + 0x0C), 0, 0); 335 collect_netvars(*(uintptr_t *)(cc + 0x0C), 0, 0);
diff --git a/src/overlay.cpp b/src/overlay.cpp
new file mode 100644
index 0000000..2445d95
--- /dev/null
+++ b/src/overlay.cpp
@@ -0,0 +1,140 @@
1// overlay.cpp — the single ISurface access layer.
2#include "hl2h.h"
3#include <ctime>
4
5ISurface *g_surface = nullptr;
6
7// ---------------------------------------------------------------------------
8// per-frame draw budget
9// ---------------------------------------------------------------------------
10
11static bool g_drawn_this_frame = false;
12static double g_frame_mark_at = 0.0;
13
14static double overlay_now() {
15 timespec ts;
16 clock_gettime(CLOCK_MONOTONIC, &ts);
17 return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
18}
19
20void overlay_frame_mark() {
21 g_drawn_this_frame = false;
22 g_frame_mark_at = overlay_now();
23}
24
25bool overlay_should_draw() {
26 if (g_drawn_this_frame && overlay_now() - g_frame_mark_at < 0.10)
27 return false;
28 g_drawn_this_frame = true;
29 return true;
30}
31
32void overlay_color(int r, int g, int b, int a) {
33 if (!g_surface)
34 return;
35 using Fn = void (*)(void *, int, int, int, int);
36 vfunc<Fn>(g_surface, SURF_DrawSetColor)(g_surface, r, g, b, a);
37}
38
39void overlay_fill(int x0, int y0, int x1, int y1) {
40 if (!g_surface)
41 return;
42 using Fn = void (*)(void *, int, int, int, int);
43 vfunc<Fn>(g_surface, SURF_DrawFilledRect)(g_surface, x0, y0, x1, y1);
44}
45
46void overlay_outline_rect(int x0, int y0, int x1, int y1) {
47 if (!g_surface)
48 return;
49 using Fn = void (*)(void *, int, int, int, int);
50 vfunc<Fn>(g_surface, SURF_DrawOutlinedRect)(g_surface, x0, y0, x1, y1);
51}
52
53void overlay_line(int x0, int y0, int x1, int y1) {
54 if (!g_surface)
55 return;
56 using Fn = void (*)(void *, int, int, int, int);
57 vfunc<Fn>(g_surface, SURF_DrawLine)(g_surface, x0, y0, x1, y1);
58}
59
60void overlay_text_font(HFont f) {
61 if (!g_surface)
62 return;
63 using Fn = void (*)(void *, HFont);
64 vfunc<Fn>(g_surface, SURF_DrawSetTextFont)(g_surface, f);
65}
66
67void overlay_text_color(int r, int g, int b, int a) {
68 if (!g_surface)
69 return;
70 using Fn = void (*)(void *, int, int, int, int);
71 vfunc<Fn>(g_surface, SURF_DrawSetTextColor)(g_surface, r, g, b, a);
72}
73
74void overlay_text_pos(int x, int y) {
75 if (!g_surface)
76 return;
77 using Fn = void (*)(void *, int, int);
78 vfunc<Fn>(g_surface, SURF_DrawSetTextPos)(g_surface, x, y);
79}
80
81void overlay_print_text(const wchar_t *text, int count, int draw_type) {
82 if (!g_surface || !text || count <= 0)
83 return;
84 using Fn = void (*)(void *, const wchar_t *, int, int);
85 vfunc<Fn>(g_surface, SURF_DrawPrintText)(g_surface, text, count, draw_type);
86}
87
88HFont overlay_create_font() {
89 if (!g_surface)
90 return 0;
91 using Fn = HFont (*)(void *);
92 return vfunc<Fn>(g_surface, SURF_CreateFont)(g_surface);
93}
94
95bool overlay_add_glyph_set(HFont f, const char *name, int tall, int weight,
96 int blur, int scanlines, int flags, int range_min,
97 int range_max) {
98 if (!g_surface || !name)
99 return false;
100 using Fn = bool (*)(void *, HFont, const char *, int, int, int, int, int, int,
101 int);
102 return vfunc<Fn>(g_surface, SURF_AddGlyphSetToFont)(
103 g_surface, f, name, tall, weight, blur, scanlines, flags, range_min,
104 range_max);
105}
106
107void overlay_screen_size(int &w, int &h) {
108 if (!g_surface)
109 return;
110 using Fn = void (*)(void *, int &, int &);
111 vfunc<Fn>(g_surface, SURF_GetScreenSize)(g_surface, w, h);
112}
113
114void overlay_text_size(HFont f, const wchar_t *text, int &w, int &h) {
115 if (!g_surface || !text)
116 return;
117 using Fn = void (*)(void *, HFont, const wchar_t *, int &, int &);
118 vfunc<Fn>(g_surface, SURF_GetTextSize)(g_surface, f, text, w, h);
119}
120
121int overlay_embedded_panel() {
122 if (!g_surface)
123 return 0;
124 using Fn = int (*)(void *);
125 return vfunc<Fn>(g_surface, SURF_GetEmbeddedPanel)(g_surface);
126}
127
128void overlay_push_make_current(int panel, bool use_insets) {
129 if (!g_surface)
130 return;
131 using Fn = void (*)(void *, int, bool);
132 vfunc<Fn>(g_surface, SURF_PushMakeCurrent)(g_surface, panel, use_insets);
133}
134
135void overlay_pop_make_current(int panel) {
136 if (!g_surface)
137 return;
138 using Fn = void (*)(void *, int);
139 vfunc<Fn>(g_surface, SURF_PopMakeCurrent)(g_surface, panel);
140}
diff --git a/src/overlay.h b/src/overlay.h
new file mode 100644
index 0000000..6cf86c9
--- /dev/null
+++ b/src/overlay.h
@@ -0,0 +1,62 @@
1#ifndef HL2H_OVERLAY_H
2#define HL2H_OVERLAY_H
3
4#include <cstdint>
5
6// VGUI_Surface030 vtable slots, confirmed against the shipped
7// vguimatsurface.so. This header is the only place that knows them; every
8// consumer goes through the functions below.
9enum SurfaceSlot {
10 SURF_GetEmbeddedPanel = 6,
11 SURF_PushMakeCurrent = 8,
12 SURF_PopMakeCurrent = 9,
13 SURF_DrawSetColor = 10,
14 SURF_DrawFilledRect = 12,
15 SURF_DrawOutlinedRect = 14,
16 SURF_DrawLine = 15,
17 SURF_DrawSetTextFont = 17,
18 SURF_DrawSetTextColor = 18,
19 SURF_DrawSetTextPos = 20,
20 SURF_DrawPrintText = 22,
21 SURF_GetScreenSize = 38,
22 SURF_CreateFont = 66,
23 SURF_AddGlyphSetToFont = 67,
24 SURF_GetTextSize = 75
25};
26
27using HFont = unsigned long;
28
29struct ISurface;
30
31// Set once by install_menu(); every wrapper below is a no-op while it is null.
32extern ISurface *g_surface;
33
34void overlay_color(int r, int g, int b, int a);
35void overlay_fill(int x0, int y0, int x1, int y1);
36void overlay_outline_rect(int x0, int y0, int x1, int y1);
37void overlay_line(int x0, int y0, int x1, int y1);
38void overlay_text_font(HFont f);
39void overlay_text_color(int r, int g, int b, int a);
40void overlay_text_pos(int x, int y);
41void overlay_print_text(const wchar_t *text, int count, int draw_type = 0);
42HFont overlay_create_font();
43bool overlay_add_glyph_set(HFont f, const char *name, int tall, int weight,
44 int blur, int scanlines, int flags, int range_min,
45 int range_max);
46void overlay_screen_size(int &w, int &h);
47void overlay_text_size(HFont f, const wchar_t *text, int &w, int &h);
48int overlay_embedded_panel();
49void overlay_push_make_current(int panel, bool use_insets);
50void overlay_pop_make_current(int panel);
51
52// Per-frame draw budget. The engine calls PaintTraverseEx for every panel, and
53// DrawPrintText for every text draw, so the overlay must be collapsed to one
54// draw per frame. A timer cannot do that: panels spaced a few ms apart in the
55// same frame would draw twice and double the alpha of translucent elements.
56// overlay_frame_mark() is called from FRAME_RENDER_START, which is the real
57// frame boundary; if that stops arriving (menu/pause) a 10 ms fallback keeps
58// the GUI repainting.
59void overlay_frame_mark();
60bool overlay_should_draw();
61
62#endif
diff --git a/src/server.cpp b/src/server.cpp
index b1661ac..0d14c59 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -68,7 +68,7 @@ static bool g_srv_checked = false;
68 68
69static uintptr_t find_string(const char *s) { 69static uintptr_t find_string(const char *s) {
70 size_t n = strlen(s) + 1; 70 size_t n = strlen(s) + 1;
71 for (auto &r : g_server.ranges) { 71 for (auto &r : mod_server().ranges) {
72 const uint8_t *base = (const uint8_t *)r.lo; 72 const uint8_t *base = (const uint8_t *)r.lo;
73 size_t span = r.hi - r.lo; 73 size_t span = r.hi - r.lo;
74 for (size_t i = 0; i + n <= span; i++) 74 for (size_t i = 0; i + n <= span; i++)
@@ -92,7 +92,7 @@ static int datamap_offsets(const char *name, int want_type, int min_off,
92 if (!str) 92 if (!str)
93 return 0; 93 return 0;
94 int n = 0; 94 int n = 0;
95 for (auto &r : g_server.ranges) { 95 for (auto &r : mod_server().ranges) {
96 const uint8_t *base = (const uint8_t *)r.lo; 96 const uint8_t *base = (const uint8_t *)r.lo;
97 size_t span = r.hi - r.lo; 97 size_t span = r.hi - r.lo;
98 for (size_t off = 4; off + 0x18 <= span; off += 4) { 98 for (size_t off = 4; off + 0x18 <= span; off += 4) {
@@ -111,7 +111,7 @@ static int datamap_offsets(const char *name, int want_type, int min_off,
111 continue; 111 continue;
112 // DEFINE_KEYFIELD entries have an external name ("health"); plain 112 // DEFINE_KEYFIELD entries have an external name ("health"); plain
113 // DEFINE_FIELD entries have none. Either way it must be in server.so. 113 // DEFINE_FIELD entries have none. Either way it must be in server.so.
114 if (ext && !g_server.contains(ext)) 114 if (ext && !mod_server().contains(ext))
115 continue; 115 continue;
116 bool dup = false; 116 bool dup = false;
117 for (int i = 0; i < n; i++) 117 for (int i = 0; i < n; i++)
@@ -144,9 +144,9 @@ bool resolve_server_netvars() {
144 if ((retry++ % 100) != 0) 144 if ((retry++ % 100) != 0)
145 return false; 145 return false;
146 // server.so is only mapped once a map is running; refresh the module list. 146 // server.so is only mapped once a map is running; refresh the module list.
147 if (!g_server.valid()) 147 if (!mod_server().valid())
148 find_modules(); 148 find_modules();
149 if (!g_server.valid()) { 149 if (!mod_server().valid()) {
150 if (!fail_logged) { 150 if (!fail_logged) {
151 fail_logged = true; 151 fail_logged = true;
152 logd("server datamap: server.so not mapped yet"); 152 logd("server datamap: server.so not mapped yet");
@@ -209,7 +209,7 @@ bool resolve_server_netvars() {
209 "followtarget=%d spawnflags=%d hacked=%d npcstate=%d " 209 "followtarget=%d spawnflags=%d hacked=%d npcstate=%d "
210 "takedamage=%d mapname=%d armor=%d detonate=%d dmgradius=%d " 210 "takedamage=%d mapname=%d armor=%d detonate=%d dmgradius=%d "
211 "ammo=%d myweapons=%d juice_suit=%d juice_health=%d", 211 "ammo=%d myweapons=%d juice_suit=%d juice_health=%d",
212 g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, 212 mod_server().path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate,
213 g_srv_modelindex, g_srv_absvelocity, g_srv_velocity, 213 g_srv_modelindex, g_srv_absvelocity, g_srv_velocity,
214 g_srv_activeweapon, g_srv_relationship, g_srv_followtarget, 214 g_srv_activeweapon, g_srv_relationship, g_srv_followtarget,
215 g_srv_spawnflags, g_srv_hacked, g_srv_npcstate, g_srv_takedamage, 215 g_srv_spawnflags, g_srv_hacked, g_srv_npcstate, g_srv_takedamage,
@@ -219,7 +219,7 @@ bool resolve_server_netvars() {
219 } else if (!fail_logged) { 219 } else if (!fail_logged) {
220 fail_logged = true; 220 fail_logged = true;
221 logf("server datamap: scan failed (server.so at %s)", 221 logf("server datamap: scan failed (server.so at %s)",
222 g_server.path.c_str()); 222 mod_server().path.c_str());
223 toastf(TOAST_WARN, "Server datamap scan failed; jumpbug assist limited"); 223 toastf(TOAST_WARN, "Server datamap scan failed; jumpbug assist limited");
224 } 224 }
225 return g_srv_ready; 225 return g_srv_ready;
@@ -275,6 +275,19 @@ int server_entity_health(int idx, int *maxhealth, int *lifestate,
275 return hp; 275 return hp;
276} 276}
277 277
278bool server_entity_velocity(int idx, Vector *out) {
279 if (!out || g_srv_velocity <= 0)
280 return false;
281 void *base = nullptr;
282 if (server_entity_health(idx, nullptr, nullptr, &base) < 0 || !base)
283 return false;
284 uintptr_t b = (uintptr_t)base;
285 out->x = *(float *)(b + g_srv_velocity);
286 out->y = *(float *)(b + g_srv_velocity + 4);
287 out->z = *(float *)(b + g_srv_velocity + 8);
288 return true;
289}
290
278int server_model_index(void *base) { 291int server_model_index(void *base) {
279 if (!base || g_srv_modelindex <= 0) 292 if (!base || g_srv_modelindex <= 0)
280 return -1; 293 return -1;
diff --git a/src/studio.cpp b/src/studio.cpp
index 3ee6cc5..69a2e07 100644
--- a/src/studio.cpp
+++ b/src/studio.cpp
@@ -68,9 +68,11 @@ const char *info_model_name(void *info) {
68 void *hdr = *(void **)info; 68 void *hdr = *(void **)info;
69 if (!hdr) 69 if (!hdr)
70 return nullptr; 70 return nullptr;
71 // studiohdr_t::name is char[64]; scanning further would read the header's
72 // length/eyeposition fields and could return a bogus "name".
71 const char *name = (const char *)hdr + 12; 73 const char *name = (const char *)hdr + 12;
72 size_t n = strnlen(name, 128); 74 size_t n = strnlen(name, 64);
73 if (n < 7 || n >= 128) 75 if (n < 7 || n >= 64)
74 return nullptr; 76 return nullptr;
75 for (size_t i = 0; i < n; i++) { 77 for (size_t i = 0; i < n; i++) {
76 unsigned char c = (unsigned char)name[i]; 78 unsigned char c = (unsigned char)name[i];
@@ -127,8 +129,13 @@ void hook_draw_model(void *self, void *results, void *info, void *bone,
127 } 129 }
128 g_orig_draw_model(self, results, info, bone, flex, flex_delayed, 130 g_orig_draw_model(self, results, info, bone, flex, flex_delayed,
129 model_origin, flags); 131 model_origin, flags);
130 if (prop) 132 if (prop) {
131 g_prop_depth--; 133 g_prop_depth--;
134 // Don't leave the prop tint bound: models drawn without an explicit
135 // SetColorModulation would inherit it.
136 const float white[4] = {1.0f, 1.0f, 1.0f, 1.0f};
137 g_orig_set_color(self, white);
138 }
132} 139}
133 140
134void hook_draw_array(void *self, void *info, int count, void *instances, 141void hook_draw_array(void *self, void *info, int count, void *instances,
@@ -158,8 +165,11 @@ void hook_draw_static_prop(void *self, void *info, void *model_to_world,
158 if (prop) 165 if (prop)
159 g_prop_depth++; 166 g_prop_depth++;
160 g_orig_draw_static(self, info, model_to_world, flags); 167 g_orig_draw_static(self, info, model_to_world, flags);
161 if (prop) 168 if (prop) {
162 g_prop_depth--; 169 g_prop_depth--;
170 const float white[4] = {1.0f, 1.0f, 1.0f, 1.0f};
171 g_orig_set_color(self, white);
172 }
163} 173}
164 174
165std::string loaded_module_path(const char *needle) { 175std::string loaded_module_path(const char *needle) {
@@ -279,7 +289,8 @@ void chams_apply(void *mat, bool ignorez, int style, bool force_white,
279 vfunc<FlagFn>(mat, 29)(mat, 1 << 6 /* SELFILLUM */, style == 5); 289 vfunc<FlagFn>(mat, 29)(mat, 1 << 6 /* SELFILLUM */, style == 5);
280} 290}
281 291
282void chams_reset(void *mat) { using FlagFn = void (*)(void *, int, bool); 292void chams_reset(void *mat) {
293 using FlagFn = void (*)(void *, int, bool);
283 vfunc<FlagFn>(mat, 29)(mat, 1 << 15, false); 294 vfunc<FlagFn>(mat, 29)(mat, 1 << 15, false);
284 vfunc<FlagFn>(mat, 29)(mat, 1 << 10, false); 295 vfunc<FlagFn>(mat, 29)(mat, 1 << 10, false);
285 vfunc<FlagFn>(mat, 29)(mat, 1 << 28, false); 296 vfunc<FlagFn>(mat, 29)(mat, 1 << 28, false);
@@ -288,6 +299,11 @@ void chams_reset(void *mat) { using FlagFn = void (*)(void *, int, bool);
288 vfunc<FlagFn>(mat, 29)(mat, 1 << 6, false); 299 vfunc<FlagFn>(mat, 29)(mat, 1 << 6, false);
289 chams_color(mat, 1.0f, 1.0f, 1.0f); 300 chams_color(mat, 1.0f, 1.0f, 1.0f);
290 chams_alpha(mat, 1.0f); 301 chams_alpha(mat, 1.0f);
302 // AlphaModulate is applied per-entity, separate from $alpha, and was left at
303 // the last chams alpha. Styles 0/3/4/6 share one material, so a translucent
304 // pass permanently half-faded it for every later draw.
305 using AlphaFn = void (*)(void *, float);
306 vfunc<AlphaFn>(mat, 27)(mat, 1.0f);
291} 307}
292 308
293// Model draws take their final alpha from IVRenderView::SetBlend; material 309// Model draws take their final alpha from IVRenderView::SetBlend; material
@@ -310,6 +326,10 @@ void hkdModelExecute(void *self, const void *state, const void *info,
310 void *bone) { 326 void *bone) {
311 if (!g_orig_model_exec) 327 if (!g_orig_model_exec)
312 return; 328 return;
329 if (!init_live()) {
330 g_orig_model_exec(self, state, info, bone);
331 return;
332 }
313 if (feature_active(FEAT_CHAMS) && info && g_force_override) { 333 if (feature_active(FEAT_CHAMS) && info && g_force_override) {
314 // ModelRenderInfo_t::entity_index is at 0x30 on 32-bit. 334 // ModelRenderInfo_t::entity_index is at 0x30 on 32-bit.
315 int idx = *(const int *)((const char *)info + 0x30); 335 int idx = *(const int *)((const char *)info + 0x30);
@@ -497,17 +517,32 @@ void chams_preload() {
497 if (now - last < 1.5) 517 if (now - last < 1.5)
498 return; 518 return;
499 last = now; 519 last = now;
520 using RefFn = void (*)(void *);
500 for (int i = 0; i < 7; i++) { 521 for (int i = 0; i < 7; i++) {
501 if (g_style_mats[i]) 522 if (g_style_mats[i])
502 continue; 523 continue;
503 void *m = world_find_material(chams_material_name(i)); 524 const char *name = chams_material_name(i);
525 // Styles 0/3/4/6 and 2/5 resolve to the same material. Reuse the pointer
526 // and reference it only once instead of incrementing the shared material
527 // up to four times.
528 void *m = nullptr;
529 for (int j = 0; j < 7; j++) {
530 if (g_style_mats[j] && strcmp(chams_material_name(j), name) == 0) {
531 m = g_style_mats[j];
532 break;
533 }
534 }
535 bool fresh = m == nullptr;
536 if (!m)
537 m = world_find_material(name);
504 if (m) { 538 if (m) {
505 // FindMaterial can hand back an unreferenced material; the engine warns 539 // FindMaterial can hand back an unreferenced material; the engine warns
506 // ("bad reference count 0") and may reset its vars when it is bound. 540 // ("bad reference count 0") and may reset its vars when it is bound.
507 using RefFn = void (*)(void *); 541 if (fresh)
508 vfunc<RefFn>(m, 12)(m); // IMaterial::IncrementReferenceCount 542 vfunc<RefFn>(m, 12)(m); // IMaterial::IncrementReferenceCount
509 g_style_mats[i] = m; 543 g_style_mats[i] = m;
510 logf("chams: preloaded style %d %s = %p", i, chams_material_name(i), m); 544 logf("chams: preloaded style %d %s = %p%s", i, name, m,
545 fresh ? "" : " (shared)");
511 } 546 }
512 } 547 }
513 if (!g_style_mats[7]) { 548 if (!g_style_mats[7]) {
@@ -535,14 +570,17 @@ void chams_init() {
535 } 570 }
536 g_force_override = (ForceOverrideFn)vt[1]; 571 g_force_override = (ForceOverrideFn)vt[1];
537 g_orig_model_exec = (ModelExecFn)vt[19]; 572 g_orig_model_exec = (ModelExecFn)vt[19];
538 if (!g_engine.contains((uintptr_t)g_force_override) || 573 if (!mod_engine().contains((uintptr_t)g_force_override) ||
539 !g_engine.contains((uintptr_t)g_orig_model_exec)) { 574 !mod_engine().contains((uintptr_t)g_orig_model_exec)) {
540 logf("chams: implausible vtable entries override=%p draw=%p", 575 logf("chams: implausible vtable entries override=%p draw=%p",
541 (void *)g_force_override, (void *)g_orig_model_exec); 576 (void *)g_force_override, (void *)g_orig_model_exec);
542 return; 577 return;
543 } 578 }
544 static void *nvt[32]; 579 void **nvt = vtable_clone(g_iface.modelrender, 32);
545 memcpy(nvt, vt, sizeof(nvt)); 580 if (!nvt) {
581 logf("chams: vtable clone failed");
582 return;
583 }
546 nvt[19] = (void *)&hkdModelExecute; 584 nvt[19] = (void *)&hkdModelExecute;
547 g_modelrender = g_iface.modelrender; 585 g_modelrender = g_iface.modelrender;
548 *(void ***)g_modelrender = nvt; 586 *(void ***)g_modelrender = nvt;
@@ -581,8 +619,11 @@ void studio_props_init() {
581 logf("studio: interface has no vtable"); 619 logf("studio: interface has no vtable");
582 return; 620 return;
583 } 621 }
584 static void *vt[49]; 622 void **vt = vtable_clone(sr, 49);
585 memcpy(vt, old_vt, sizeof(vt)); 623 if (!vt) {
624 logf("studio: vtable clone failed");
625 return;
626 }
586 627
587 g_orig_set_color = (SetColorFn)vt[27]; 628 g_orig_set_color = (SetColorFn)vt[27];
588 g_orig_set_alpha = (SetAlphaFn)vt[28]; 629 g_orig_set_alpha = (SetAlphaFn)vt[28];
diff --git a/src/vischeck.cpp b/src/vischeck.cpp
index 6b531b9..6126c88 100644
--- a/src/vischeck.cpp
+++ b/src/vischeck.cpp
@@ -88,12 +88,29 @@ struct EntityFilter : ITraceFilterCompat {
88 int GetTraceType() const override { return 0; } // TRACE_EVERYTHING 88 int GetTraceType() const override { return 0; } // TRACE_EVERYTHING
89}; 89};
90 90
91// True when pEntity is a child of target: held weapons are separate entities
92// and heavy NPCs use phys_bone_follower entities for their collision model, so
93// a trace toward an NPC's torso otherwise stops on the weapon/follower and the
94// target reads as occluded. CBaseHandle stores the entity index in the low
95// bits, so the owner handle resolves through the client entity list.
96static bool owned_by(void *pEntity, void *target) {
97 if (!pEntity || !target || g_net.owner < 0)
98 return false;
99 uint32_t h = *(uint32_t *)((uintptr_t)pEntity + g_net.owner);
100 int idx = (int)(h & 0x1FFFu);
101 if (idx <= 0)
102 return false;
103 return entitylist_get(idx) == target;
104}
105
91struct VisibilityFilter : ITraceFilterCompat { 106struct VisibilityFilter : ITraceFilterCompat {
92 void *self; 107 void *self;
93 void *target; 108 void *target;
94 VisibilityFilter(void *s, void *t) : self(s), target(t) {} 109 VisibilityFilter(void *s, void *t) : self(s), target(t) {}
95 bool ShouldHitEntity(void *pEntity, int) override { 110 bool ShouldHitEntity(void *pEntity, int) override {
96 return pEntity != self && pEntity != target; 111 if (pEntity == self || pEntity == target)
112 return false;
113 return !owned_by(pEntity, target);
97 } 114 }
98 int GetTraceType() const override { return 0; } // TRACE_EVERYTHING 115 int GetTraceType() const override { return 0; } // TRACE_EVERYTHING
99}; 116};
@@ -176,14 +193,14 @@ bool is_visible(const Vector &eye, const Vector &point, void *self,
176 // Trace everything that bullets care about: world, static props, crates and 193 // Trace everything that bullets care about: world, static props, crates and
177 // NPCs. Non-solid glass and glass whose material name marks it breakable is 194 // NPCs. Non-solid glass and glass whose material name marks it breakable is
178 // skipped; solid non-break glass (bulletproof) blocks. 195 // skipped; solid non-break glass (bulletproof) blocks.
196 using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *,
197 GameTraceCompat *);
179 Vector start = eye; 198 Vector start = eye;
180 for (int pass = 0; pass < 5; pass++) { 199 for (int pass = 0; pass < 5; pass++) {
181 RayCompat ray; 200 RayCompat ray;
182 make_ray(ray, start, point); 201 make_ray(ray, start, point);
183 GameTraceCompat tr{}; 202 GameTraceCompat tr{};
184 VisibilityFilter filter(self, target); 203 VisibilityFilter filter(self, target);
185 using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *,
186 GameTraceCompat *);
187 vfunc<TraceRayFn>(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray, 204 vfunc<TraceRayFn>(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray,
188 MASK_SHOT, &filter, &tr); 205 MASK_SHOT, &filter, &tr);
189 if (tr.startsolid || tr.allsolid) 206 if (tr.startsolid || tr.allsolid)
@@ -196,11 +213,34 @@ bool is_visible(const Vector &eye, const Vector &point, void *self,
196 bool solid = (tr.contents & CONTENTS_SOLID) != 0; 213 bool solid = (tr.contents & CONTENTS_SOLID) != 0;
197 bool breakable_glass = glass && surface_is_breakable_glass(tr.surface); 214 bool breakable_glass = glass && surface_is_breakable_glass(tr.surface);
198 if (glass && (!solid || breakable_glass)) { 215 if (glass && (!solid || breakable_glass)) {
199 // Step just past the pane and look for the next obstruction. 216 // Glass brushes are commonly 1-4 units thick, so the old fixed 0.5-unit
200 start = tr.endpos; 217 // step landed inside the brush and the next pass reported startsolid,
201 start.x += dir.x * 0.5f; 218 // which read as "blocked". Advance until a probe from the new start is
202 start.y += dir.y * 0.5f; 219 // clear, doubling the step, and stop if we pass the target.
203 start.z += dir.z * 0.5f; 220 Vector next = tr.endpos;
221 float step = 1.0f;
222 bool clear = false;
223 for (int guard = 0; guard < 16 && !clear; guard++) {
224 next.x += dir.x * step;
225 next.y += dir.y * step;
226 next.z += dir.z * step;
227 float ox = next.x - point.x, oy = next.y - point.y,
228 oz = next.z - point.z;
229 if (ox * ox + oy * oy + oz * oz < 0.25f)
230 return true; // stepped past the target; nothing left to block it
231 RayCompat probe;
232 make_ray(probe, next, point);
233 GameTraceCompat ptr{};
234 VisibilityFilter pfilter(self, target);
235 vfunc<TraceRayFn>(g_iface.enginetrace, 4)(
236 g_iface.enginetrace, &probe, MASK_SHOT, &pfilter, &ptr);
237 clear = !ptr.startsolid && !ptr.allsolid;
238 if (!clear)
239 step *= 2.0f;
240 }
241 if (!clear)
242 return false; // still inside geometry; treat as blocked
243 start = next;
204 logd("vischeck: skipped glass surface=%s contents=0x%x solid=%d break=%d", 244 logd("vischeck: skipped glass surface=%s contents=0x%x solid=%d break=%d",
205 tr.surface.name ? tr.surface.name : "?", tr.contents, (int)solid, 245 tr.surface.name ? tr.surface.name : "?", tr.contents, (int)solid,
206 (int)breakable_glass); 246 (int)breakable_glass);
@@ -270,7 +310,11 @@ bool point_ground(const Vector &point, void *ignore) {
270 vfunc<TraceRayFn>(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray, 310 vfunc<TraceRayFn>(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray,
271 MASK_PLAYERSOLID, &filter, &tr); 311 MASK_PLAYERSOLID, &filter, &tr);
272 // A start inside geometry means a wall/step there, not a ledge. 312 // A start inside geometry means a wall/step there, not a ledge.
273 return tr.startsolid || tr.fraction < 1.0f; 313 if (tr.startsolid)
314 return true;
315 // Only a standable surface counts: a steep cliff face hit within 96 units is
316 // not a floor, and treating it as one suppressed the edge jump.
317 return tr.fraction < 1.0f && tr.normal.z >= 0.7f;
274} 318}
275 319
276// Vertical distance from the standing player hull's feet to the first 320// Vertical distance from the standing player hull's feet to the first
diff --git a/src/world.cpp b/src/world.cpp
index ea186b4..a0576ab 100644
--- a/src/world.cpp
+++ b/src/world.cpp
@@ -633,6 +633,10 @@ bool load_png_rgba(const char *path, std::vector<unsigned char> &pixels,
633} 633}
634 634
635std::vector<unsigned char> g_sky_pixels; 635std::vector<unsigned char> g_sky_pixels;
636// Pristine copy of the decoded PNG. HDR compensation is applied to
637// g_sky_pixels in place, so without this the linearised/darkened bytes bled
638// into the next map when it used the LDR sky shader.
639std::vector<unsigned char> g_sky_pixels_src;
636int g_sky_px_w = 0, g_sky_px_h = 0; 640int g_sky_px_w = 0, g_sky_px_h = 0;
637 641
638// ITextureRegenerator has two virtuals: RegenerateTextureBits then Release. 642// ITextureRegenerator has two virtuals: RegenerateTextureBits then Release.
@@ -717,6 +721,28 @@ bool is_sky_material(const char *name, const char *group) {
717 strncmp(name, "skybox/", 7) == 0; 721 strncmp(name, "skybox/", 7) == 0;
718} 722}
719 723
724// ITexture reference counting (material system header order, matching the
725// slots already used here: SetTextureRegenerator = 12, Download = 13):
726// 10 = IncrementReferenceCount, 11 = DecrementReferenceCount
727// The engine owns the original textures; holding a reference keeps them valid
728// between "capture" and "restore" instead of leaving a dangling pointer.
729static void texture_addref(void *tex) {
730 if (!tex)
731 return;
732 using Fn = void (*)(void *);
733 vfunc<Fn>(tex, 10)(tex);
734}
735
736void release_sky_originals() {
737 for (auto &r : g_sky_orig) {
738 if (!r.texture)
739 continue;
740 using Fn = void (*)(void *);
741 vfunc<Fn>(r.texture, 11)(r.texture);
742 }
743 g_sky_orig.clear();
744}
745
720void record_orig(const char *material, const char *var, const char *value, 746void record_orig(const char *material, const char *var, const char *value,
721 void *texture) { 747 void *texture) {
722 if (!material || !var) 748 if (!material || !var)
@@ -725,6 +751,7 @@ void record_orig(const char *material, const char *var, const char *value,
725 if (r.material == material && r.var == var) 751 if (r.material == material && r.var == var)
726 return; 752 return;
727 } 753 }
754 texture_addref(texture);
728 g_sky_orig.push_back( 755 g_sky_orig.push_back(
729 {material, var, value ? value : "", texture}); 756 {material, var, value ? value : "", texture});
730} 757}
@@ -750,6 +777,10 @@ int apply_sky_texture() {
750 // where the materials are definitely loaded, and re-upload if needed. 777 // where the materials are definitely loaded, and re-upload if needed.
751 bool hdr = sky_uses_hdr_shader(); 778 bool hdr = sky_uses_hdr_shader();
752 if (hdr && !g_sky_compensated && !g_sky_pixels.empty()) { 779 if (hdr && !g_sky_compensated && !g_sky_pixels.empty()) {
780 // Always start from the untouched decode so repeated HDR passes cannot
781 // compound the gamma conversion.
782 if (!g_sky_pixels_src.empty())
783 g_sky_pixels = g_sky_pixels_src;
753 for (size_t i = 0; i + 3 < g_sky_pixels.size(); i += 4) { 784 for (size_t i = 0; i + 3 < g_sky_pixels.size(); i += 4) {
754 for (int c = 0; c < 3; c++) { 785 for (int c = 0; c < 3; c++) {
755 float v = (float)g_sky_pixels[i + (size_t)c] / 255.0f; 786 float v = (float)g_sky_pixels[i + (size_t)c] / 255.0f;
@@ -822,7 +853,7 @@ void restore_sky_texture() {
822 } 853 }
823 void *ms = material_system(); 854 void *ms = material_system();
824 if (!ms) { 855 if (!ms) {
825 g_sky_orig.clear(); 856 release_sky_originals();
826 g_sky_applied = false; 857 g_sky_applied = false;
827 return; 858 return;
828 } 859 }
@@ -833,7 +864,7 @@ void restore_sky_texture() {
833 auto count = vfunc<MatCalls::CountFn>(ms, MATSYS_GetNumMaterials); 864 auto count = vfunc<MatCalls::CountFn>(ms, MATSYS_GetNumMaterials);
834 int total = count(ms); 865 int total = count(ms);
835 if (total <= 0 || total > 200000) { 866 if (total <= 0 || total > 200000) {
836 g_sky_orig.clear(); 867 release_sky_originals();
837 g_sky_applied = false; 868 g_sky_applied = false;
838 return; 869 return;
839 } 870 }
@@ -866,7 +897,7 @@ void restore_sky_texture() {
866 vfunc<SetStrFn>(var, 5)(var, r.value.c_str()); 897 vfunc<SetStrFn>(var, 5)(var, r.value.c_str());
867 } 898 }
868 } 899 }
869 g_sky_orig.clear(); 900 release_sky_originals();
870 g_sky_applied = false; 901 g_sky_applied = false;
871} 902}
872 903
@@ -925,6 +956,7 @@ void load_sky_texture() {
925 return; 956 return;
926 } 957 }
927 g_sky_pixels.swap(pixels); 958 g_sky_pixels.swap(pixels);
959 g_sky_pixels_src = g_sky_pixels;
928 g_sky_px_w = w; 960 g_sky_px_w = w;
929 g_sky_px_h = h; 961 g_sky_px_h = h;
930 962
@@ -960,6 +992,13 @@ void skybox_invalidate() {
960 992
961void skybox_reload_image() { 993void skybox_reload_image() {
962 restore_sky_texture(); 994 restore_sky_texture();
995 // Detach the regenerator before dropping the handle: the old texture may
996 // outlive it and must not call back into a regenerator whose pixels were
997 // replaced.
998 if (g_sky_tex) {
999 using SetRegenFn = void (*)(void *, void *);
1000 vfunc<SetRegenFn>(g_sky_tex, 12)(g_sky_tex, nullptr);
1001 }
963 g_sky_tex = nullptr; 1002 g_sky_tex = nullptr;
964 g_sky_tex_tried = false; 1003 g_sky_tex_tried = false;
965 g_sky_reload = true; 1004 g_sky_reload = true;