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