# 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. ## Runtime architecture - Modules from `/proc/self/maps`: `client.so`, `engine.so`, `server.so`. PROT_NONE ranges excluded; file-backed bss tails merged. - Calling convention: cdecl (i386); `this` is the first stack argument. - Hooks (copy-and-swap vtable, 256 slots): - `CHLClient` slot 21 `CreateMove`; slot 35 `FrameStageNotify`. - `ISurface` slot 114 `PaintTraverseEx` (primary draw); slot 22 `DrawPrintText` fallback. Draws wrapped in `PushMakeCurrent`/`PopMakeCurrent`. - `CInput*` discovered by byte-scanning the `CHLClient::CreateMove` prologue. Usercmd CRC-32 is repaired after any cmd edit (`note_modified`). - Console output via `ConMsg` from `libtier0.so` (`_Z6ConMsgPKcz`), not `IVEngineClient::ClientCmd`. ## Interfaces / indices | Interface | Version | Notes | |---|---|---| | Client | `VClient017` | | | Entity list | `VClientEntityList003` | | | Engine | `VEngineClient014` | | | Model info | `VModelInfoClient006` | | | Engine server | `VEngineServer021` | edicts / listen-server entities | | Debug overlay | `VDebugOverlay003` | accurate world->screen | | Engine trace | `EngineTraceClient003` | vischeck / trigger ray | | Surface | `VGUI_Surface030` | | Relevant vtable slots (this build): - Engine: `GetLocalPlayer` 12, `Time` 14, `GetViewAngles` 19, `SetViewAngles` 20. - Entity list: `GetClientNetworkable` 0, `GetClientEntity` 3, `GetHighestEntityIndex` 6. - `IClientNetworkable`: `GetClientClass` 2. `ClientClass::m_pNetworkName` at +8. - `IClientUnknown`: `GetClientRenderable` 6. - `IClientRenderable`: `GetModel` 9, `SetupBones` 16. - `IVModelInfo`: `GetModel` 2, `GetModelName` 4, `GetStudiomodel` 29. - `IVEngineServer`: `PEntityOfEntIndex` 19. Edict `m_pUnk` at +0xC; `IServerUnknown::GetBaseEntity` 6. - `IVDebugOverlay`: `ScreenPosition` 9. - `IEngineTrace`: `TraceRay` 4. `Ray_t` uses 16-byte `VectorAligned`; `trace_t`: `fraction` 44, `startsolid` 55, `m_pEnt` 76. ## Netvars / entity data (this build) | Field | Client offset | |---|---| | `m_fFlags` | `0x33C` | | `m_vecVelocity[0]` | `0xE0` | | `m_MoveType` (prediction datamap) | `0x164` | | `m_iHealth` | `0x80` | | `m_iMaxHealth` | not networked | | `m_iTeamNum` | `0x88` | | `m_vecOrigin` | `0x324` | | `m_vecViewOffset[0]` | `0xD4` | | `m_vecMins` / `m_vecMaxs` | `0x1A8` / `0x1B4` | | `m_lifeState` | `0x7F` | | `m_nModelIndex` | `0x7C` | | `m_vecAbsVelocity` (client local, not netvar) | `0x11C` | | `m_hActiveWeapon` (client netvar; server fallback `0x824`) | resolved by name | | `m_hOwnerEntity` / `m_hOwner` (held weapons) | resolved by name | - Eye position = `m_vecOrigin + m_vecViewOffset` on **all three axes**. (The old code added the X component to Z, which left the eye ~64 units low.) - NPC health is not networked. Alive state comes from the networked client `m_lifeState`; real HP is read from the listen-server `CBaseEntity` found via `PEntityOfEntIndex -> edict->m_pUnk -> GetBaseEntity`, using offsets scanned from `server.so`'s datamap: - `m_iHealth` `0xF4`, `m_iMaxHealth` `0xF0`, `m_lifeState` `0xF8`, `m_nModelIndex` `0x7A` (short). ## Menu There is a single menu: the custom GUI, toggled with `Insert`. The old text-tree menu was removed after the GUI reached parity. Every feature still has an active flag and a bind (SDL scancode; mouse buttons 600..604; None = always-on), and all settings are persisted in the INI described under GUI vocabulary / decisions. ## GUI vocabulary / decisions - **GUI**: custom immediate-mode settings surface drawn through the existing `ISurface` paint path. No Dear ImGui, no OpenGL hook, no new runtime dependency. - **Category**: sidebar group. Movement, Combat, Visuals, Weapons, System. - **Card**: per-feature panel. Header always shows Active + Bind; body is a collapsible set of settings. - **Text layout**: measured with `ISurface::GetTextSize` and wrapped to the available width. Card headers, control rows, info rows, tooltips and toasts grow/wrap instead of truncating; there is no ellipsis truncation. The config path is shown on the System page (not duplicated in the footer). - **Freeze**: while the GUI is open, look/attack/use/impulse/weapon selection are blocked and combat features are skipped, but normal WASD/jump/duck/walk movement is allowed. Mouse deltas are discarded before `CInput::CreateMove` can apply them (`CInput::ClearStates`), so the view cannot drift for a frame. Bind capture blocks all game input until it ends. The game keeps simulating and ESP/FOV circle/speed graph keep drawing. No invulnerability. - **Native cursor**: on open, leave SDL relative-mouse mode and show the OS cursor; restore on close. Absolute mouse coordinates are used. - **Toast**: transient top-right notification drawn by the overlay even when the GUI is closed. Info/warn/error colors; lifetimes 6/9/12 s. Toasts queued before the first paint (load errors) start their timer when first drawn. Used for the load message, config problems, missing interfaces/netvars, bind changes, Reload and Reset. - **Bind capture**: click Bind, press a key or any mouse button; Esc cancels capture but does not close the GUI; the small x clears the bind (None = always-on). Insert is reserved for the GUI. - **UI scale**: `clamp(min(screen_w/1920, screen_h/1080), 0.75, 1.5)`; base window 960x620, body/title fonts 15/20 px at scale 1. - Every feature gets Active + Bind; bind None means always-on. - System page: real config path/build info, Reload config (re-reads the INI), and a two-click Reset that restores feature settings/binds but keeps GUI layout. - Window: fixed-size, draggable by title bar, clamped to screen, position, category and expanded cards persisted under `[gui]` in the same INI. Esc does not close the GUI; the toggle key (Insert) is the only close path. There is no title-bar X. - GUI controls write the shared feature state; sliders persist when the drag ends, toggles/binds/layout persist immediately. `[gui]` stores `x`, `y`, `category` and an `expanded` bitmask (card index order). Every feature now persists an `active` and `bind` pair, including movement, melee and spread. - Config writes are atomic: `config.tmp` is written and flushed, then `rename()`d over the real file. A failed write raises an error toast. ## 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. - ABH: on the landing/jump tick, face opposite the horizontal velocity with zero forward/side movement; HL2's jump speed-bound subtraction then accelerates along the velocity. Camera restored at `FRAME_RENDER_START`. ## Melee killbot - Active only while the local player's active weapon is the crowbar. The client `m_hActiveWeapon` handle is used first; if it is missing, the authoritative server `m_hActiveWeapon` handle (0x824) is used to get the weapon entity index. - Crowbar range is `CROWBAR_RANGE` 75 units from `Weapon_ShootPosition`. The selector computes the closest point on each hostile NPC's AABB to the eye, rejects anything farther than 75, sorts by that distance, and picks the nearest visible one. - The usercmd view is pointed at the target for one command with `IN_ATTACK`; movement keys are rotated with `preserve_movement`, and the rendered view is restored immediately and at `FRAME_RENDER_START` (same silent-aim path). This gives a 360-degree silent melee attack. - While the crowbar is active and the killbot feature is on, the gun aimbot, triggerbot and silent aim are skipped to avoid fighting over the usercmd view. - Vischeck uses the shared `MASK_VISIBLE`/world-only path (static props are ignored). ## Spread control - HL2's `CBaseEntity::FireBullets` seeds its RNG from `CBaseEntity::GetPredictionRandomSeed(info.m_bUseServerRandomSeed)`; HL2 weapons leave `m_bUseServerRandomSeed` false, so the server uses the client usercmd's `random_seed` (masked to 8 bits). This is the TF2-style client-side seed hook. - `Spread control -> No spread` precomputes, with the engine's own `RandomSeed`/`RandomFloat`, the 8-bit seed whose `CShotManipulator::ApplySpread` offset is smallest. It sets `cmd->random_seed` on attack commands; `note_modified` repairs the CRC. - 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. - 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. ## 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. ## Gotchas / lessons - Never pipe-mask build failures: `./build.sh > /tmp/build.log 2>&1; echo rc=$?`. - `CHLClient` slot numbers: `CreateMove` 21, `FrameStageNotify` 35. - `ISurface` binary slot = SDK header slot + 4; see `src/ISURFACE_STATUS.md`. - Silent aim and ABH temporarily replace the usercmd view; silent aim preserves movement, ABH intentionally zeroes it for the jump tick. - Aimbot vischeck (`MASK_VISIBLE`/world-only) differs from triggerbot (`MASK_SHOT`/all entities): glass and static props can behave differently. ## Valuable items - Visuals feature; Active is on by default and unbound (always-on). - Scans the client entity list every 100 ms and tags health, ammo, armor/suit, world weapon pickups, the two supply-crate models and generic items. - Each tag is a colored marker with a name and optional distance, filtered by category toggles and a max-distance slider (default 5000 units). Supply crates ignore the distance limit. 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. ## 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.