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
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
|
# 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, HDR brightness) 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. The scan builds
local Module snapshots and publishes them under a mutex, because the setup
thread and the server-netvar retry can call it concurrently (resetting the
shared vectors live used to crash at startup).
- 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.
- **Dropdown**: `CK_DROPDOWN` row with a value button; clicking opens an
overlay list below the row (above it near the window bottom). Clicking an
option sets the value and saves the config; clicking elsewhere closes it.
One dropdown is open at a time; it is not clipped by the card/content rect.
- **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, hunter-chopper: 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`). With the RPG
equipped, armoured candidates are sorted before normal enemies (a
`fov - 1000` rank key), so a strider/gunship is acquired even when a normal
NPC is closer to the crosshair.
- 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`).
- `world_to_screen` rejects non-finite projections and absurd magnitudes
(>1e7). A point on the view axis (e.g. directly below the player's feet)
can make `IVDebugOverlay::ScreenPosition` return NaN/huge values; casting
those to int corrupted the ISurface clip state and made the whole overlay
disappear for that frame. If the overlay reports "behind" for a point whose
view-space depth is actually positive, a local projection is used instead.
- NPC boxes are built from several vertical samples, so a target whose feet
are behind the near plane (standing over it) still gets a box from the
visible span instead of being dropped.
- Projectile radius rings are drawn independently of the centre marker, so a
grenade behind the camera/off-screen still shows its blast circle when any
part of the ring projects.
- 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` is a client cvar, applied through `ClientCmd` when its
slider changes.
- `HDR brightness` (`[view] tonemap`, x10, 0 = automatic) sets
`mat_force_tonemap_scale`. This is in-engine tonemapping for HDR maps and
does not depend on the window system; 10 = 1.0. It has no visible effect
while Fullbright is on (fullbright bypasses tonemapping).
- `mat_monitorgamma` was tried and removed: the engine applies the ramp (SDL
receives changing midpoints), but Wayland/XWayland compositors discard
window gamma ramps, so the slider did nothing on the development setup.
- Both controls live in the always-on `Camera / display` card
(`[view] fov/vmfov/tonemap`).
## 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. Each edge is clipped at the camera near plane and then to the
screen, so a box straddling the camera still draws its visible portions.
## Chams
- Hooks `IVModelRender::DrawModelExecute` (slot 19 in `VEngineModel016`).
The model's entity is `ModelRenderInfo_t::entity_index` (32-bit offset 0x30).
NPCs are grouped by `esp_npc_relation` (hostile / neutral / friendly);
viewmodel entities (class contains `viewmodel` or model path `.../v_...`)
are a fourth group.
- Each relationship has independent **visible** and **through-wall** settings:
on/off, style, colour and alpha. The viewmodel has on/off, style, colour and
alpha only. The through-wall pass uses `MATERIAL_VAR_IGNOREZ` (1 << 15)
through `IMaterial::SetMaterialVarFlag` (slot 29); colours use
`$color`/`$color2` (IMaterialVar slot 11, SetVecValue 3-float slot 11) and
`ColorModulate` (slot 28). Alpha is applied with
`IVRenderView::SetBlend` (`VEngineRenderView014` slot 4, saved/restored
around the passes) — model draws take their final alpha from the render
view, not from material `$alpha`. `$alpha`/`AlphaModulate` are still set for
completeness. Shared materials are restored after each draw.
- The GUI colour picker (`add_color_alpha`) has an alpha bar, checkerboard
preview and `#RRGGBBAA` readout; plain `add_color` keeps the old opaque
three-component behaviour. Chams colours are four ints (r, g, b, a).
- Alpha needs a translucent model material, so the mod writes a tiny VMT:
`<game>/<mod>/materials/hl2hake/translucent.vmt` (and a copy under
`custom/hl2hake/...`): `UnlitGeneric` with `$basetexture sprites/white`,
`$color`, `$model 1`, `$translucent 1`, `$nocull 1`. It is written from a
preload constructor (so it exists before the material system scans) and
loaded with `FindMaterial`; styles 0/3/4/6 switch to it when the colour
alpha is below 255. The base-game `debugtranslucentsinglecolor` has no
`$translucent` and cannot do alpha; `debugtranslucentvertexcolor` and
`debug/debughitbox` need vertex colours and draw nothing on skinned models.
- Styles: 0 Flat (`debug/debugtranslucentsinglecolor` + FLAT), 1 Wireframe
(`debug/debugwireframe`, brightened tint), 2 Additive
(`debug/debugtranslucentmodelhulls`, whose VMT has `$additive 1` + `$color`;
the ADDITIVE flag on the solid material did nothing), 3 Solid, 4 White
(solid, colour forced white), 5 Fullbright (the additive material, colour
forced white, giving a white-hot glow), 6 Solid + contour (solid fill plus
a `debug/debugwireframe` contour pass drawn with ZNEARER (1 << 10) and a
brightened colour, so the entity is recoloured while its edges stay
readable). Style 6 is the requested "recolour the whole entity but show its
contours" look.
`vgui/white` is vertex-colour based and has no `$model`/`$basetexture`, so it
is never used for chams; `models/debug/debugwhite` is EP2-only and the old
VGUI additive material was invisible on NPCs.
- Through-wall + visible wireframe: a wireframe visible pass leaves gaps, so
the through-wall fill showed inside the visible silhouette. When both passes
are on and the visible style is Wireframe, a solid fill of the visible
colour is drawn first, then the brightened wireframe contour. (The proper
alternative is stencil: visible writes stencil 1, through-wall tests
stencil == 0, as Amalgam does; the fill is simpler and gives the
solid+contour look.)
- Style materials are preloaded from `outer_draw` (throttled) and cached; the
render hook never calls FindMaterial itself. A missing style uses solid.
`FindMaterial` can return an unreferenced material, so each cached material
is addref'd (`IncrementReferenceCount`, IMaterial slot 12); without this the
engine warns "bad reference count 0 when being bound" and the material's
`$color`/`ColorModulate` changes are ignored.
- Additive styles still wash out against a bright sky or wall (inherent to
additive blending).
- Feature is off by default. Config sections: `[chams]` (active/bind),
`[chams_hostile|neutral|friendly]` (`vis`, `vis_mat`, `vis_r/g/b/a`, `inv`,
`inv_mat`, `inv_r/g/b/a`) and `[chams_vm]` (`on`, `mat`, `r/g/b/a`). The GUI
has a Hostile/Neutral/Friendly section each, plus Viewmodel.
- This is a material override, not stencil outlines.
## 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 or the
gravity gun. 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.
- With the gravity gun, only `VULNERABLE` targets (headcrab, antlion,
manhack) are selected, the range is 1200 units, and the attack button is
`IN_ATTACK`: the primary attack launches a held object along
`EyeVectors()`. No aim-punch compensation is applied there (EyeVectors does
not add punch); the crowbar path keeps it. Without a held object the primary
is a short physics punch. The citadel supercharges the gravity gun
(`d3_citadel_03`, `d3_citadel_04`, `d3_breen_01`; detected through
`IVEngineClient::GetLevelName`, slot 51), and there it is treated as a
normal weapon: the `VULNERABLE`-only filter is dropped, so any damageable
hostile NPC is selected.
- 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 either melee weapon 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).
- "Only if usable" (`[items] usable=1`) hides health pickups when the local
player is at full health and armor pickups at full suit. The health ceiling
is the server player's `m_iMaxHealth` (not a hardcoded 100); armor uses the
HL2 `MAX_NORMAL_BATTERY` cap of 100, read from the server
`m_ArmorValue`. If the values cannot be read the filter fails open.
- A world weapon whose type is already in the local player's inventory is
reclassified as ammo (`SMG ammo`, `AR2 ammo`, ...). Owner handles are
unreliable on this client (`m_hOwnerEntity`/`m_hOwner` resolve to -1), so
the inventory is read from the authoritative server
`CBaseCombatCharacter::m_hMyWeapons[48]` array (client `m_hMyWeapons` and the
remembered active weapon are fallbacks). NPC-held weapons are ignored.
Weapons the player does not own stay under the Weapons category.
- Ammo pickups also honour "Only if usable": the reserve count from the
authoritative server `CBaseCombatCharacter::m_iAmmo` array is compared with
the `sk_max_*` cvar (stock caps as fallback, e.g. RPG rockets = 3); the
client array is only a fallback. Full ammo hides the pickup; if either value
cannot be read the item stays visible. Preferring the server array fixed
"full" counts flickering as other ammo was spent — the client `m_iAmmo`
netvar offset can be off by an element.
- Frag grenade pickups were falling through to a generic "Ammo" label with
`ammo_type = -1`, so the usable filter never applied and they always showed.
`models/weapons/w_grenade`, `w_frag`, `items/grenade` and `frag_grenade` are
now labelled "Frag grenade" and mapped to `m_iAmmo[11]` (`sk_max_grenade`,
stock 5). AR2/SMG1 grenade variants map to `m_iAmmo[8]` (stock 3).
- The datamap offset for `m_iAmmo`/`m_hMyWeapons` points at the
`CNetworkArray` length header; the actual elements start **4 bytes later**.
Reading at the raw offset shifted every count by one type (e.g. RPG read the
buckshot count 30 against a max of 3, permanently hiding rockets).
- Chargers and ammo/supply crates are dispensers and are exempt from the
fullness filter. Empty **suit** chargers are hidden (`CNewRecharge`
`m_iJuice` at 0x47c, derived from `m_iMaxJuice - 20`; 0 = depleted, -1
unreadable fails open). Health chargers are currently left visible: their
candidate `m_iJuice` offset (0x3c8 for `CWallHealth`/`CNewWallHealth`) read 0
on a full charger, so the empty-hide is not trusted yet.
`server_item_juice` logs up to eight `juice:` lines with the class and both
candidate values to finish diagnosing this. Item classification also falls
back to the server class name when the client class is generic, and the
health branch accepts `healthcharger` (maps use `item_healthcharger`,
i.e. `CNewWallHealth`, not the `func_healthcharger` brush).
- 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.
## Projectile ESP
- Visuals feature `g_projectiles` (`[projectiles]`, Active on, unbound).
- Scans the client entity list every 100 ms for world-model `w_grenade`,
`w_missile*`, `helicopter_bomb` and `combineball`. Weapon entities are
excluded by class (`...weapon...`); thrown grenades are recognised by
`CGrenadeFrag`-style class names or by an owner handle.
- Fuse: `CBaseGrenade::m_flDetonateTime` is not networked, so it is read from
the server entity datamap (`FIELD_TIME`) and compared with
`IVEngineClient::Time()` (slot 14). RPG rockets have no fuse. If the server
value or the engine time is unavailable, a 3-second countdown from the first
sighting is used so the label still ticks. The first 8 projectiles log
`det=`, `cur=`, the server base and the raw fuse value for diagnosis.
- Blast radius: networked client `m_DmgRadius` when available, server
`m_DmgRadius` (`FIELD_FLOAT`) as fallback, then 300/200/150/300 for
frag/RPG/AR2/bomb.
- Draws a small marker and a `FRAG 1.2s`-style label; the optional radius ring
is a 36-segment horizontal circle projected at the projectile height, with
every segment near-plane/screen clipped independently, so it still draws
when the grenade itself is off-screen or behind the camera. Segments whose
clipped endpoint the engine overlay refuses fall back to the local
projection, which now uses the configured camera FOV (not a hardcoded 75°),
so the ring does not kink or drop pieces at wide FOVs.
Toggles: Fuse timer, Blast radius, Grenade trajectory
(`[projectiles] trajectory=1`).
- Grenade trajectory: while the frag weapon is active, simulate the throw
from `weapon_frag.cpp` (eye + forward*18 + right*8, player velocity +
(forward + 0.1 z)*1200), gravity 600, dt 0.02, restitution 0.35, friction
0.35. Each step traces with `trace_solid_segment` (EngineTraceClient003 slot
4, `MASK_PLAYERSOLID`, local player ignored); hits reflect about the impact
normal, and an upward normal with a slow vertical speed marks the landing.
The polyline, landing cross and a 300-unit blast-radius ring at the landing
are drawn with the same near-plane-clipped projector as the radius ring.
The path keeps 512 points (one every other 20 ms step, up to 24 s), so it no
longer stops after ~2.5 s. Once a local frag has been thrown it is preferred
over the pre-throw estimate: `CBaseGrenade::m_hThrower` (server
`FIELD_EHANDLE`, with the client owner handles as fallback) identifies it, and
its live position/velocity seed the prediction. Bounces are still
approximate: the real frag is a VPhysics box with a random Y angular impulse
(`AngularImpulse(600, random(-1200,1200), 0)`), so contact points and
post-bounce paths differ between throws from the same spot. Verified on
`:99` (pre-throw arc + landing ring; the line now reaches the landing).
## 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.
|