1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
|
# Context: HL2 Linux Internal Cheat (`hl2hake`)
## What this is
A 32-bit Linux internal for Half-Life 2 (native `hl2_linux`), loaded with
`LD_PRELOAD` through Steam's bundled 32-bit runtime. It started as a port of
the Windows `hake` reference (auto-bhop / auto-ABH / auto-climb) and now also
covers ESP, aimbot, triggerbot and silent aim.
## Build / run
- `./build.sh` -> `libhl2hake.so`
(`g++ -m32 -O2 -std=c++17 -Wall -Wextra -Werror -fPIC -shared`,
sources: `hl2h modules cinput netvars menu esp server bones vischeck`, `-ldl`).
- `./play.sh` -> Steam runtime + `hl2.sh -steam -novid -console +map d1_town_01`.
- Logs: `/tmp/hl2hake.log` (`logf`) and stderr. `./play.sh > /tmp/hl2run.log 2>&1`
captures everything.
- Debug env:
- `HL2H_DRAW_TEST=1` — red probe rect + text, no menu needed.
- `HL2H_BONE_DEBUG=1` — white marker at each NPC head hitbox.
- `HL2H_NO_SERVER=1` — disable server-side health reads.
- Config: `$XDG_CONFIG_HOME/hl2hake/config`, falling back to
`~/.config/hl2hake/config`. Plain INI, saved on every change. New sections:
`[view]` (camera FOV, viewmodel FOV, gamma x10) and `[movement]`
(crouch-jump, edge jump).
## Runtime architecture
- Modules from `/proc/self/maps`: `client.so`, `engine.so`, `server.so`.
PROT_NONE ranges excluded; file-backed bss tails merged.
- Calling convention: cdecl (i386); `this` is the first stack argument.
- Hooks (copy-and-swap vtable, 256 slots):
- `CHLClient` slot 21 `CreateMove`; slot 35 `FrameStageNotify`.
- `ISurface` slot 114 `PaintTraverseEx` (primary draw);
slot 22 `DrawPrintText` fallback. Draws wrapped in
`PushMakeCurrent`/`PopMakeCurrent`.
- `CInput*` discovered by byte-scanning the `CHLClient::CreateMove` prologue.
Usercmd CRC-32 is repaired after any cmd edit (`note_modified`).
- Console output via `ConMsg` from `libtier0.so`
(`_Z6ConMsgPKcz`), not `IVEngineClient::ClientCmd`.
## Interfaces / indices
| Interface | Version | Notes |
|---|---|---|
| Client | `VClient017` | |
| Entity list | `VClientEntityList003` | |
| Engine | `VEngineClient014` | |
| Model info | `VModelInfoClient006` | |
| Engine server | `VEngineServer021` | edicts / listen-server entities |
| Debug overlay | `VDebugOverlay003` | accurate world->screen |
| Engine trace | `EngineTraceClient003` | vischeck / trigger ray |
| Surface | `VGUI_Surface030` | |
Relevant vtable slots (this build):
- Engine: `GetLocalPlayer` 12, `Time` 14, `GetViewAngles` 19, `SetViewAngles` 20.
- Entity list: `GetClientNetworkable` 0, `GetClientEntity` 3,
`GetHighestEntityIndex` 6.
- `IClientNetworkable`: `GetClientClass` 2. `ClientClass::m_pNetworkName` at +8.
- `IClientUnknown`: `GetClientRenderable` 6.
- `IClientRenderable`: `GetModel` 9, `SetupBones` 16.
- `IVModelInfo`: `GetModel` 2, `GetModelName` 4, `GetStudiomodel` 29.
- `IVEngineServer`: `PEntityOfEntIndex` 19.
Edict `m_pUnk` at +0xC; `IServerUnknown::GetBaseEntity` 6.
- `IVDebugOverlay`: `ScreenPosition` 9.
- `IEngineTrace`: `TraceRay` 4. `Ray_t` uses 16-byte `VectorAligned`;
`trace_t`: `fraction` 44, `startsolid` 55, `m_pEnt` 76.
## Netvars / entity data (this build)
| Field | Client offset |
|---|---|
| `m_fFlags` | `0x33C` |
| `m_vecVelocity[0]` | `0xE0` |
| `m_MoveType` (prediction datamap) | `0x164` |
| `m_iHealth` | `0x80` |
| `m_iMaxHealth` | not networked |
| `m_iTeamNum` | `0x88` |
| `m_vecOrigin` | `0x324` |
| `m_vecViewOffset[0]` | `0xD4` |
| `m_vecMins` / `m_vecMaxs` | `0x1A8` / `0x1B4` |
| `m_lifeState` | `0x7F` |
| `m_nModelIndex` | `0x7C` |
| `m_vecAbsVelocity` (client local, not netvar) | `0x11C` |
| `m_hActiveWeapon` (client netvar; server fallback `0x824`) | resolved by name |
| `m_hOwnerEntity` / `m_hOwner` (held weapons) | resolved by name |
- Eye position = `m_vecOrigin + m_vecViewOffset` on **all three axes**.
(The old code added the X component to Z, which left the eye ~64 units low.)
- NPC health is not networked. Alive state comes from the networked client
`m_lifeState`; real HP is read from the listen-server `CBaseEntity` found via
`PEntityOfEntIndex -> edict->m_pUnk -> GetBaseEntity`, using offsets scanned
from `server.so`'s datamap:
- `m_iHealth` `0xF4`, `m_iMaxHealth` `0xF0`,
`m_lifeState` `0xF8`, `m_nModelIndex` `0x7A` (short).
## Menu
There is a single menu: the custom GUI, toggled with `Insert`. The old
text-tree menu was removed after the GUI reached parity. Every feature still
has an active flag and a bind (SDL scancode; mouse buttons 600..604; None =
always-on), and all settings are persisted in the INI described under GUI
vocabulary / decisions.
## GUI vocabulary / decisions
- **GUI**: custom immediate-mode settings surface drawn through the existing
`ISurface` paint path. No Dear ImGui, no OpenGL hook, no new runtime
dependency.
- **Category**: sidebar group. Movement, Combat, Visuals, Weapons, System.
- **Card**: per-feature panel. Header always shows Active + Bind; body is a
collapsible set of settings.
- **Text layout**: measured with `ISurface::GetTextSize` and wrapped to the
available width. Card headers, control rows, info rows, tooltips and toasts
grow/wrap instead of truncating; there is no ellipsis truncation. The config
path is shown on the System page (not duplicated in the footer).
- **Freeze**: while the GUI is open, look/attack/use/impulse/weapon selection
are blocked and combat features are skipped, but normal WASD/jump/duck/walk
movement is allowed. Mouse deltas are discarded before `CInput::CreateMove`
can apply them (`CInput::ClearStates`), so the view cannot drift for a frame.
Bind capture blocks all game input until it ends. The game keeps simulating
and ESP/FOV circle/speed graph keep drawing. No invulnerability.
- **Native cursor**: on open, leave SDL relative-mouse mode and show the OS
cursor; restore on close. Absolute mouse coordinates are used.
- **Toast**: transient top-right notification drawn by the overlay even when
the GUI is closed. Info/warn/error colors; lifetimes 6/9/12 s. Toasts queued
before the first paint (load errors) start their timer when first drawn.
Used for the load message, config problems, missing interfaces/netvars,
bind changes, Reload and Reset.
- **Bind capture**: click Bind, press a key or any mouse button; Esc cancels
capture but does not close the GUI; the small x clears the bind (None =
always-on). Insert is reserved for the GUI.
- **UI scale**: `clamp(min(screen_w/1920, screen_h/1080), 0.75, 1.5)`; base
window 960x620, body/title fonts 15/20 px at scale 1.
- Every feature gets Active + Bind; bind None means always-on.
- System page: real config path/build info, Reload config (re-reads the INI),
and a two-click Reset that restores feature settings/binds but keeps GUI
layout.
- Window: fixed-size, draggable by title bar, clamped to screen, position,
category and expanded cards persisted under `[gui]` in the same INI. Esc
does not close the GUI; the toggle key (Insert) is the only close path.
There is no title-bar X.
- GUI controls write the shared feature state; sliders persist when
the drag ends, toggles/binds/layout persist immediately. `[gui]` stores
`x`, `y`, `category` and an `expanded` bitmask (card index order). Every
feature now persists an `active` and `bind` pair, including movement, melee
and spread.
- All card contents are clipped to the scrolling content rect, including
checkboxes and the expand chevron; only clipped fills/text were handled
before, so cards scrolled out of view leaked header widgets outside the
window.
- **Colour control (`CK_COLOR`)**: row shows a live swatch; clicking the row
expands an inline picker (saturation/value square + hue bar + hex/RGB
preview) instead of three RGB sliders, growing the card in place so it
scrolls and clips like any other row. The picker keeps HSV state so grey
colours do not lose their hue; dragging writes the 0-255 RGB triple shared
with the config and saves on mouse release. Only one picker is open at a
time; closing the GUI or collapsing the card hides it.
- Config writes are atomic: `config.tmp` is written and flushed, then
`rename()`d over the real file. A failed write raises an error toast.
## NPC friendliness
- ESP colours, aimbot/silent-aim/melee target filtering use the server AI
relationship (`m_Relationship` CUtlVector) against the local player:
entity-specific rows first, then the CLASS_PLAYER class row. D_LI is
friendly, D_NU neutral, D_HT/D_FR hostile.
- This makes map-scripted friendly turrets and other relationship overrides
dynamic instead of model-name guesses. When no row exists, the old model
heuristic is used.
- `m_Relationship` is declared with `DEFINE_UTLVECTOR`, so its datamap entry
is `FIELD_CUSTOM` (not `FIELD_EMBEDDED`). Resolving it as embedded silently
returned `-1` for every NPC.
- Floor turrets are special: citizen-built turrets (`SF_FLOOR_TURRET_CITIZEN`,
spawnflags `0x200`) and Alyx-hacked turrets (`m_bHackedByAlyx`) report
`CLASS_PLAYER_ALLY` from their overridden `Classify()` and never add a
relationship row. They are classified from those server fields instead.
- Bugbait-controlled antlions are also treated as friendly when their server
`m_hFollowTarget` is set, covering antlions that spawned before the global
`antlion_allied` state changed.
- Stock NPCs have no `m_Relationship` rows at all: their disposition comes from
the global `CHL2GameRules::InitDefaultRelationships()` table indexed by
`Classify()` (e.g. `CLASS_PLAYER_ALLY -> D_LI`, `CLASS_HEADCRAB -> D_HT`).
That table is not stored per entity, so `relation=-1` is normal and the
model heuristic is the fallback for those NPCs (Kleiner, Combine, ...).
- `m_NPCState` (`FIELD_INTEGER`, server datamap) is read as well:
`NPC_STATE_SCRIPT` (4) marks scripted/passive actors as neutral, so they are
not aimbot targets and draw with the neutral colour. Lamarr
(`models/Lamarr.mdl`) is additionally listed as neutral by model.
## Damage classification
- Every NPC is classified for ESP and target selection:
- `IMMUNE` — server `m_takedamage` is `DAMAGE_NO`/`DAMAGE_EVENTS_ONLY`.
- `ARMORED` — gunship, strider, dropship: their `OnTakeDamage` only accepts
blast damage (`DMG_BLAST`, with a damage threshold on gunships and a
distance check on striders). Only the RPG passes; bullets/grenades do not.
- `VULNERABLE` — headcrab, antlion, manhack: punting them with the gravity
gun / physics impact damage kills them.
- `NORMAL` — everything else.
- ESP draws a small 10 px label on the left edge of the box with a black 1 px
shadow: `ARM` (orange), `VULN` (cyan) or `IMM` (grey). Toggle is
`ESP → Damage tags` (`[esp] dmg=1`). The small font is a second `HFont`
created at glyph size 10 alongside the 16 px body font (`g_font_small`).
- Aimbot/silent aim/triggerbot/melee skip `IMMUNE` targets always and
`ARMORED` targets unless the active weapon is the RPG
(`active_weapon_contains` for `rocket_launcher`/`rpg`).
- Server `m_takedamage` is a `char` in the 2013 SDK
(`CNetworkVarForDerived(char, m_takedamage)`, `FIELD_CHARACTER`), so it is
resolved with that type (int fallback) and read as a byte. `0/1` means no
damage.
## ESP additions
- Alert state: server `m_NPCState` is drawn as a second small left label
(`IDLE` grey, `ALERT` yellow, `COMBAT` red, `SCRIPT` blue). Toggle
`ESP -> Alert state` (`alert=1`).
- Enemy weapon: the NPC's server `m_hActiveWeapon` handle gives the weapon
entity, whose server model index is mapped to a short label (`SMG1`, `AR2`,
`Shotgun`, `RPG`, ...) with the client class name as fallback. Toggle
`ESP -> Enemy weapon` (`weapon=1`).
- Colours: hostile / neutral / friendly box, skeleton and name colours are
three `CK_COLOR` pickers in the ESP card (`hr/hg/hb`, `nr/ng/nb`,
`fr/fg/fb`). Health bars keep their semantic red/yellow/green.
- Off-screen arrows: hostiles whose head projection is outside the screen get
a chevron clamped to a 26 px margin; behind-the-camera targets use the
dot product with the view right vector. Toggle `ESP -> Off-screen arrows`
(`arrows=1`).
- Map transitions: see below.
## Camera / display
- The rendered camera FOV comes from `CViewRender::OnRenderStart` copying
`C_BasePlayer::m_iDefaultFOV` into `default_fov`, so the cheat writes the
client netvar every tick. The camera slider is 40..140 (stock 75) and the
viewmodel slider 20..120 (stock 54); 0 is not allowed (old configs with 0
migrate to the stock value).
- `viewmodel_fov` and `mat_monitorgamma` are client cvars; applied through
`ClientCmd` when their slider changes. Gamma is stored as x10
(`[view] gamma=22` = 2.2). The gamma value is read back through
`ICvar::FindVar` and logged (`view: gamma request ..., cvar reports ...`)
to distinguish a rejected command from an OS gamma ramp the compositor
ignores. Both live in the always-on `Camera / display` card
(`[view] fov/vmfov/gamma`).
## Map transitions
- Server-only brush entities: iterate `IVEngineServer::GetEntityCount` /
`PEntityOfEntIndex`, ask `IServerNetworkable::GetClassName` (slot 3) for
`trigger_changelevel`, read `CChangeLevel::m_szMapName`
(`FIELD_CHARACTER`, char[32]) as the next map, and get the volume from
`ICollideable::WorldSpaceSurroundingBounds` (slot 17, no virtual dtor; the
interface comes from `IServerUnknown::GetCollideable`, slot 4).
- The current map name comes from the worldspawn (`entity 0`) model name
(`maps/<name>.bsp`). The map before the last change is kept as
`g_map_prev`, so each trigger's own target is drawn as `-> map`, and the
trigger whose target equals `g_map_prev` is marked `<- map` (the way back).
- Scanning runs at 4 Hz in the ESP draw; the 8 world corners are projected and
the 12 edges are drawn as a 3D wireframe, with a 10 px tag above the highest
visible corner.
## Bones / ESP
- `SetupBones(BONE_USED_BY_ANYTHING)` with engine `Time()`; studio header via
`GetStudiomodel`; hitboxes and bone names/parents from that header.
- HL2 strips many ValveBiped bone names (the zombie's spine/legs/left arm are
unnamed), so the humanoid skeleton is built from the **union of hitbox bone
parent paths** (`HITGROUP_*`). Head group 1 includes the zombie headcrab body
and its `HC_Head` child; fingers, toes and other `HC_*` limbs are excluded.
- Non-humanoid NPCs (headcrabs, crows, antlions, ...) draw the full bone tree.
- `aim_points` = head hitbox/bone + hitbox centers for the selected groups;
the aim features pick the nearest visible point.
## Aim features
- Shared target selection (`select_aim_target`): live hostile NPCs, skip
friendly/neutral, rank by angle to the crosshair, then pick the nearest
selected hitgroup point. Target candidates are tried nearest-first.
- Vischeck: `EngineTraceClient003::TraceRay`, `MASK_VISIBLE`,
`TRACE_WORLD_ONLY` (world only; static props are not tested — deliberate).
- Aimbot: bounded smoothing — 0.05 deg deadzone, max `Smoothness` deg/tick,
shortest-path yaw, pitch clamped to ±89; pushes the new view with
`SetViewAngles`.
- Triggerbot: crosshair ray with `MASK_SHOT` + `TRACE_EVERYTHING` (filter
ignores the local player). First hit must be a live hostile NPC; sets
`IN_ATTACK`.
- Silent aim: exact target angle written to `cmd->viewangles`, `IN_ATTACK`,
then the rendered view is restored immediately **and** at
`FRAME_RENDER_START`; `forwardmove`/`sidemove` are rewritten to preserve the
world-space movement wish vector. Because `IN_ATTACK` is set after
`CInput::CreateMove`, the minimal-spread seed is applied again in `do_move`
(same for triggerbot). Crossbow bolts and RPG rockets are led by target
velocity and travel time; the crossbow also gets its 0.05 gravity drop.
- Silent aim reuses the Aimbot FOV, vischeck and bone selection (it has no
separate FOV any more); only its bind and active flag are separate.
- Aim punch: the server's bullet direction is `EyeAngles() +
m_Local.m_vecPunchAngle` (`CBasePlayer::GetAutoaimVector`). When the new
"Compensate aim punch" toggle is on, the aim/silent/melee target angle has
the client `m_vecPunchAngle` subtracted, and the triggerbot traces along
`view + punch`, so the shot lands on the crosshair target during recoil.
- ABH: on the landing/jump tick, face opposite the horizontal velocity with
zero forward/side movement; HL2's jump speed-bound subtraction then
accelerates along the velocity. Camera restored at `FRAME_RENDER_START`.
## Melee killbot
- Active only while the local player's active weapon is the crowbar. The
client `m_hActiveWeapon` handle is used first; if it is missing, the
authoritative server `m_hActiveWeapon` handle (0x824) is used to get the
weapon entity index.
- Map transitions often switch the active weapon away from the crowbar. The
state change is logged as `melee: active ... crowbar=0/1` (normal log), and
a one-time toast says to equip the crowbar. If the client weapon model is
missing/0, the server model index is used to recognize it.
- Bone caches and aim locks are cleared when the local player entity changes
(map load/respawn).
- Crowbar range is `CROWBAR_RANGE` 75 units from `Weapon_ShootPosition`. The
selector computes the closest point on each hostile NPC's AABB to the eye,
rejects anything farther than 75, sorts by that distance, and picks the
nearest visible one.
- The usercmd view is pointed at the target for one command with `IN_ATTACK`;
movement keys are rotated with `preserve_movement`, and the rendered view is
restored immediately and at `FRAME_RENDER_START` (same silent-aim path).
This gives a 360-degree silent melee attack.
- While the crowbar is active and the killbot feature is on, the gun aimbot,
triggerbot and silent aim are skipped to avoid fighting over the usercmd
view.
- Vischeck uses the shared `MASK_VISIBLE`/world-only path (static props are
ignored).
## Spread control
- HL2's `CBaseEntity::FireBullets` seeds its RNG from
`CBaseEntity::GetPredictionRandomSeed(info.m_bUseServerRandomSeed)`; HL2
weapons leave `m_bUseServerRandomSeed` false, so the server uses the
client usercmd's `random_seed` (masked to 8 bits). This is the TF2-style
client-side seed hook.
- `Spread control -> No spread` precomputes, with the engine's own
`RandomSeed`/`RandomFloat`, the 8-bit seed whose
`CShotManipulator::ApplySpread` offset is smallest. It sets
`cmd->random_seed` on attack commands; `note_modified` repairs the CRC.
Silent aim and triggerbot set `IN_ATTACK` after the CInput hook, so they
re-apply the best seed in `do_move`.
- Shotguns use consecutive seeds per pellet. The primary's first pellet is
`FIRE_BULLETS_FIRST_SHOT_ACCURATE`, so the table excludes it; the secondary
uses all 12. Separate best starting seeds are precomputed for 1, 8 and 12
pellets.
- The server `RandomSeed` PLT slot is found at runtime by reading the loaded
`server.so` ELF relocation table (`module_jump_slot`): the anniversary
branch's `hl2_complete/bin/server.so` moved the `R_386_JMP_SLOT` from
`0xce1348` (older `bin/server.so`) to `0xce5348`, and the hardcoded patch
silently hit unrelated data. The hook overrides the seed whenever the
`RandomSeed` caller's `this` is the local server player, i.e.
`CBaseEntity::FireBullets`; the `CUniformRandomStream` wrappers and
constructor-time `RandomSeed(0)` resets pass other objects and are left
alone.
- `Spread control -> Engine spread override` (config `engine=1`) gates the
direct zeroing; when off, the RandomFloat hook passes through and only the
seed search is used.
- Seed choice alone cannot remove all of a shotgun's pellet spread: the best
of 256 sequences still leaves its 8/12 draws apart. `CShotManipulator::
ApplySpread` is inlined into `CBaseEntity::FireBullets` and draws x/y via
the global `CUniformRandomStream` (`_ZTV20CUniformRandomStream` + 8, slot 1
= `RandomFloat(float,float)`; confirmed against `libvstdlib.so`). The hook
returns 0 for those draws so every pellet follows the exact shot direction.
Detection is frame-based (`ebp+8` = the local server player) *and* a small
armed window: the player's `FireBullets` `RandomSeed` call sets a counter of
8 draws, which the next `RandomFloat` calls consume. The window is required
because the compiler can outline the `ApplySpread` draws into a different
frame, where the first argument is not the player. NPC fire and the client's
prediction stream keep normal values.
- This attacks the random cone offset only. The cone size, the pistol's
`m_flAccuracyPenalty` growth, the server view punch added to
`GetAutoaimVector`, and any damage falloff are server-side state. In
single-player those can be written/hooked locally; stock HL2 has no
distance damage falloff.
## Jump assist
- **Auto crouch-jump** (`[movement] crouchjump`): while airborne with the
jump button still held and upward velocity, adds `IN_DUCK`; releases it once
the ascent ends. Only the bit we added is removed, so a player who holds
crouch keeps it. Bhop clears `IN_JUMP` in the air, so this applies to manual
jumps.
- **Edge jump** (`[movement] edgejump`): on ground, moving >120 u/s, probes a
point 0.12 s ahead with a downward `MASK_PLAYERSOLID` trace. No floor within
96 units sets `IN_JUMP`. A startsolid probe (wall/step) counts as ground, so
running into walls does not bounce. Both are simple toggles, not features
with binds.
## Jumpbug
- Automatic while falling when the predicted landing speed
`sqrt(vz^2 + 2*600*x)` exceeds `PLAYER_MAX_SAFE_FALL_SPEED` (526.5).
- The engine only applies fall damage when `CheckFalling` sees a ground entity
at the end of the frame. `PlayerMove` runs `Duck()` **before**
`FullWalkMove`'s `CheckJumpButton`, so a same-frame `FinishUnDuck` sets ground
via `CategorizePosition`, the jump executes, and the fall-damage test is
skipped.
- In air, `FinishDuck` raises the origin by 36 (standing hull 72, duck hull 36)
and `CanUnduck` only succeeds when the floor is at least 36 below; after
unducking, `CategorizePosition` needs it within 2 more units. The unduck
window is therefore a 2-unit band `[36,38]` above the floor.
- The feature presses duck when the standing distance `x` is within
`drop + [0,2]` (where `drop` is the previous command's measured fall), then
releases duck + `IN_JUMP` on the next command when `x` is in `[36,38]`.
It also clears `IN_JUMP` while waiting so the pogo latch is clear.
- `drop` is measured from consecutive predicted origins (not a hard-coded tick
interval). At high fall speeds the 2-unit band can be skipped between
commands; a higher frame rate improves reliability. Best-effort by design.
- **Assist timing** (off by default): when the natural next command would jump
over the 2-unit window, force the post-command distance to 37 by writing the
vertical velocity. It needs both the authoritative server velocity and the
client prediction velocity:
- server `CBaseEntity::m_vecAbsVelocity` (datamap offset 0x228), read by
`SetupMove` via `GetAbsVelocity`;
- client `C_BaseEntity::m_vecAbsVelocity` (datamap offset 0x11C) and the
networked `m_vecVelocity` (0xE0 + 8).
The velocity is `dz / 0.015 + 0.5 * 600 * 0.015` where `dz` is the required
displacement for this command. This turns the ~20% timing success at 625 ups
into a repeatable setup, at the cost of one command with a nudged vertical
speed. Pure jumpbug remains available with Assist off.
- The server datamap has two `m_vecVelocity` entries (0x1C and 0x29C), so the
assist deliberately writes only `m_vecAbsVelocity`; `SetAbsVelocity` syncs
`m_vecVelocity` after the command.
## World transformations
### Fullbright
- Visuals card with the usual Active + Bind. Unbound and Active means always
on; a bind makes it a hold-to-brighten key.
- Applies the client `mat_fullbright` cvar to 1/0 through
`IVEngineClient::ClientCmd` (slot 7), which bypasses the console cheat gate
and avoids the crashing direct `ConVar` vtable path. No server-side writes.
- Re-applied after map loads/respawns (`fullbright_invalidate` on local entity
change) and saved as `[fullbright] active/bind`.
- `fullbright: on/off` is logged; a missing cvar interface raises an error
toast instead of failing silently.
### World tint (material colour modulation)
- Same idea as Amalgam's world modulation: find `IMaterialSystem` in the
already-loaded `materialsystem.so` and walk every material with
`FirstMaterial` / `NextMaterial` / `GetMaterial`.
- `IMaterial::ColorModulate(r,g,b)` is applied to materials whose
`GetTextureGroupName()` is `World textures` (sky names excluded) and to
`SkyBox textures` (or names containing `sky`). Lightmaps are not touched.
- Props are NOT tinted through their materials: VertexLitGeneric materials get
their colour modulation from `IStudioRender`, which the engine sets per
model draw (white), so the material var is overridden. `src/studio.cpp`
hooks the `VStudioRender025` interface object instead:
`SetColorModulation`/`SetAlphaModulation` (slots 27/28), `DrawModel` (29),
`DrawModelStaticProp` (30) and `DrawModelArray` (46), all checked against
`CStudioRenderContext`'s vtable in `studiorender.so`.
`DrawModel` checks `DrawModelInfo_t::m_pStudioHdr->name`; note the name is
the short form with Windows separators (`props_junk\wood_crate002a.mdl`,
no `models/` prefix), so prop detection matches `props/`, `props\` and
`props_` after stripping `models/` (excluding `props_skybox`). For a prop it
sets the prop colour right before calling the original draw; setting the
colour from a flag inside the draw does not work because the engine calls
`SetColorModulation(1,1,1)` *before* `DrawModel`.
Do not force `SetAlphaModulation` there: the earlier attempt produced
error-material props and a material-system crash.
- `DrawModel` takes seven arguments after `this`: results, info, boneToWorld,
flexWeights, flexDelayedWeights, `const Vector &modelOrigin` (arrives as a
pointer) and `flags`. The hook must forward all of them; omitting
`modelOrigin` shifts `flags` into its slot and leaves the real `flags` as
caller stack garbage, which corrupted roughly every other prop draw
(checkerboard / error-material props). Verified against `studiorender.so`
0x83ad0: the callee reads `0x20(%ebp)` (modelOrigin) and `0x24(%ebp)`
(flags). `DrawModelStaticProp` takes info/modelToWorld/flags only.
- `prop_tint_update()` also writes `m_clrRender` on prop entities (dynamic
props) as a secondary path; the netvar resolver rejects offset 0 (an
embedded-table false positive) and only accepts 0x20..0x2000.
- Slot map (2013 SDK layout, registered under `VMaterialSystem080`):
`IMaterialSystem` 73/74/75/76/77 = First/Next/Invalid/Get/GetNumMaterials;
`IMaterial` 0/1/28 = GetName/GetTextureGroupName/ColorModulate.
- `src/world.cpp`; the walk runs only when the enabled groups or colours
change, and re-applies after level changes via `world_tint_invalidate()`.
Disabling restores white, so no reload is needed.
- Validation before any walk: `GetNumMaterials()` must return 1..200000; the
first material's name/group must be printable strings. A mismatch logs and
toasts instead of poking garbage.
- Saved as `[worldtint] active/bind/world/sky/prop/wr..pb`.
- Particle tint is deliberately not implemented: HL2's old particle manager
draws per-vertex coloured IMesh quads, and SpriteCard/TF2-style particles
take colour from per-particle vertex data, so `IMaterial::ColorModulate`
does nothing for them. It needs a hook in the client particle render path.
### Fog
- `src/world.cpp` `fog_update()`, driven by the `fog_override` engine command
family through `IVEngineClient::ClientCmd` (same safe path as fullbright).
No render hooks.
- Enabled: `fog_override 1`, `fog_enable 1`, `fog_color`, `fog_start`,
`fog_end`, plus `fog_enableskybox` / `fog_colorskybox` /
`fog_startskybox` / `fog_endskybox` when skybox fog is on.
"Remove fog" issues `fog_enable 0` instead.
- Disabled: `fog_override 0` restores the map's own fog controller.
- End is clamped above Start. Re-applied on level change via
`fog_invalidate()`. Saved as `[fog] active/bind/r/g/b/start/end/skybox/disable`.
### Skybox image
- Replaces the map skybox with `<config dir>/skybox.png` (same directory as
the INI). Not `sv_skyname`: that did not take effect at runtime in this
build, so the skybox card now works on materials instead.
- The PNG is decoded by our own reader (`zlib`'s `uncompress` + PNG filters;
8-bit non-interlaced RGB/RGBA/gray/palette) and scaled to the next power of
two, because the material system rejects textures whose dimensions are not
multiples of 4 ("Image dimensions must be multiple of 4!"). Do **not** use the system
libpng: `client.so`, `engine.so` and `vguimatsurface.so` each export their
own `png_create_read_struct`, so libpng16's internal PLT calls bind to the
game's older libpng with a different `png_struct` layout and crash in
`png_safe_warning` (error_ptr at +0xb0 reads NULL).
- This build's `IMaterialSystem` interface ends at `GetDisplayDeviceName`
(the slots after it belong to `CMaterialSystem` itself), so there is **no
`CreateTextureFromBits`**. The pixels are uploaded through
`CreateProceduralTexture` (vtable 81) + an `ITextureRegenerator`
(`SetTextureRegenerator` 12, `Download` 13), which fills mip 0 through
`IVTFTexture::ImageData(frame,face,mip)` (39). `IVTFTexture`'s virtual
destructor occupies two slots, so Width=15, Height=16, RowSizeInBytes=19.
- The texture is assigned with `IMaterialVar::SetTextureValue` to
`$basetexture` and `$hdrbasetexture` on every material in the `SkyBox
textures` group or named `skybox/*`. The same image maps onto all six faces.
- HDR sky materials use the `sky` shader, whose texture binding happens in
`SHADER_INIT`: it samples `$hdrcompressedtexture` (DXT5) when defined and
otherwise `$hdrbasetexture`. Putting an RGBA8 texture into the DXT5 slot
renders black, so `$hdrcompressedtexture` (+ its `0/1/2` variants) is
`SetUndefined()` and `$hdrbasetexture` is pointed at our texture instead.
The shader state is then rebuilt with `IMaterial::Refresh()` (slot 37),
otherwise the old bindings stay cached.
- Original textures are recorded per material/var (texture pointer plus the
string as a fallback) and restored when the feature is disabled. Records
survive level changes so a reused sky material can still be restored; the
override is re-applied to the new map automatically.
- `skybox_update()` runs from the `FRAME_RENDER_START` FrameStageNotify hook,
not from the VGUI paint hook: `ITexture::Download` only regenerates texture
bits on a render-safe frame stage. Running it during paint skipped the
regenerator and left an empty VTF (upload warnings + crash).
- The card shows the image path and has a Reload image button. A missing or
unreadable PNG raises an error toast; the image is only re-read on Reload.
- Saved as `[skybox] active/bind`.
## Logging
- `/tmp/hl2hake.log` is opened once at startup with `"w"` (so it only contains
the current session), line-buffered, and closed via `atexit`.
- `logf` is always on: lifecycle, warnings/errors, config, GUI open/close and
item-crate detections.
- `logd` is off unless `HL2H_DEBUG=1`; it carries per-scan ESP/NPC/item-prop,
aimbot-target, bone and netvar noise. Run `HL2H_DEBUG=1 ./play.sh` to enable.
## Gotchas / lessons
- Never pipe-mask build failures:
`./build.sh > /tmp/build.log 2>&1; echo rc=$?`.
- `CHLClient` slot numbers: `CreateMove` 21, `FrameStageNotify` 35.
- Networked EHANDLE values use an 11-bit entity index in the low bits; bits
11+ are the serial. Mask with `0x7FF`, not the local `CBaseHandle` 16-bit
mask. This caused active-weapon lookups to produce bogus indices after a
map change once the weapon serial was nonzero (melee killbot looked dead).
- `ISurface` binary slot = SDK header slot + 4; see `src/ISURFACE_STATUS.md`.
- GUI font handles are validated with `GetTextSize` and recreated when the
local player entity or UI scale changes; if the scaled size fails, 16 px is
used as a fallback. VGUI can silently invalidate font handles across level
transitions.
- Silent aim and ABH temporarily replace the usercmd view; silent aim preserves
movement, ABH intentionally zeroes it for the jump tick.
- Aimbot/silent/melee vischeck now traces everything bullets care about
(`MASK_SHOT`) while ignoring the local player and target. Static props,
crates and other NPCs block. Non-solid glass and breakable-glass materials
are skipped; solid non-break glass (bulletproof) blocks. Triggerbot still
uses the same `MASK_SHOT` trace and will not fire through glass.
## Valuable items
- Visuals feature; Active is on by default and unbound (always-on).
- Scans the client entity list every 100 ms and tags health, ammo, armor/suit,
world weapon pickups, the two supply-crate models and generic items.
- Each tag is a colored marker with a name and optional distance, filtered by
category toggles and a max-distance slider (default 5000 units; 0 means
unlimited and displays as "All"). Drawn through walls and independently of
the hostile NPC ESP.
- Ammo labels are specific where the model is known: pistol, SMG, AR2, .357,
crossbow, shotgun, sniper, flares, AR2 grenade, RPG rocket, plus ammo-crate
variants. Unknown ammo falls back to "Ammo".
- Weapons are world pickups only: the active weapon and anything with a valid
`m_hOwner`/`m_hOwnerEntity` handle is skipped.
- Only `item_item_crate` and `item_beacon_crate` are tagged as supply crates.
Other breakable crates are intentionally ignored; crate chunk debris is
skipped.
- Client `CPhysicsProp` entities often have `m_nModelIndex == 0`, including
item crates. The item scan falls back to the server entity's model index
(`server_model_index`) to recover the model name.
## Roadmap / TODO
- **Server-side spread/recoil control**: not planned; recoil and accuracy
penalties are intentionally left stock.
- **Distance weapon spread thesis**: addressed by Spread control; stock HL2
hitscan damage is range-independent, so distance efficiency is the cone.
|