# 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/.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: `//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 `/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.