# 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 | - 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 Starts fully collapsed on first open. `Insert` toggles; `Up`/`Down` navigate; `Enter` expands/toggles; `Left`/`Right` edit values; `Escape` closes. Bind rows enter bind-select mode: press a key/button to assign, `Escape` sets None (always-on), `Enter` cancels. ``` HL2hake Bunnyhop Active Uni-directional auto-strafe ABH Active Jumpbug Active Assist timing Melee killbot Active Spread control Active No spread ESP Active Show neutral / healthbar / healthnum / name / skeleton / box / speed graph Aimbot Active Bind Vischeck Bones -> Head / Torso / Left arm / Right arm / Left leg / Right leg Smoothness (1..45 deg/tick, Left/Right) FOV (1..180 deg, Left/Right) Show FOV circle Triggerbot Active Bind Silent aim Active Bind FOV (1..180 deg, Left/Right) ``` Binds use SDL scancodes; mouse buttons are pseudo-scancodes 600..604. ## 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. ## Roadmap / TODO - **GUI**: replace/augment the VGUI ISurface menu with an immediate-mode GUI (Dear ImGui or a custom implementation); needs a design grill first. - **Server-side spread/recoil control**: optional single-player writes to the pistol accuracy penalty and view punch, and a true zero-cone hook if wanted. - **Distance weapon spread thesis**: addressed by Spread control; stock HL2 hitscan damage is range-independent, so distance efficiency is the cone.