From aa096fe6886681d3338573cd3af779b57a5b56ab Mon Sep 17 00:00:00 2001 From: salmonik Date: Sun, 30 Aug 2026 03:32:59 +0200 Subject: init --- .gitignore | 1 + CONTEXT.md | 47 +++++ build.sh | 9 + play.sh | 14 ++ src/cinput.cpp | 142 +++++++++++++++ src/hl2h.cpp | 307 +++++++++++++++++++++++++++++++ src/hl2h.h | 133 ++++++++++++++ src/menu.cpp | 555 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/modules.cpp | 69 +++++++ src/netvars.cpp | 185 +++++++++++++++++++ tree | 9 + 11 files changed, 1471 insertions(+) create mode 100644 .gitignore create mode 100644 CONTEXT.md create mode 100755 build.sh create mode 100755 play.sh create mode 100644 src/cinput.cpp create mode 100644 src/hl2h.cpp create mode 100644 src/hl2h.h create mode 100644 src/menu.cpp create mode 100644 src/modules.cpp create mode 100644 src/netvars.cpp create mode 100644 tree diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73aa73c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +libhl2hake.so diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..21128f7 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,47 @@ +# Context: HL2 Linux Internal Cheat (`hl2hake`) + +## What is this project? + +A minimal, featureless cheat for Half-Life 2 (native Linux build, 32-bit `hl2_linux`). Originally ported from a Windows reference (`hake`) that implemented auto-bhop, auto-ABH, and auto-climb via a `CHLClient::CreateMove` vtable hook. + +## Resolved terms + +- **Build**: LD_PRELOAD a 32-bit `.so` into `hl2_linux` via Steam's bundled 32-bit runtime (`steam-runtime/run.sh`). +- **Hook**: Copy-and-swap vtable on the `CHLClient` instance (slot 21 = `CreateMove`). 256-slot copy to cover all dispatched virtuals. +- **Netvars**: RecvTable walk from `IBaseClientDLL::GetAllClasses` (vtable slot 8). Prediction datamap scan for `CBaseEntity::m_MoveType` (networked `movetype` is a virtual proxy with offset 0). +- **CInput**: Discovered via byte-scan of `CHLClient::CreateMove` prologue (gcc PIC `call thunk; add $imm,%esi` idiom), resolving the module-level `IInput*` variable. +- **UserCMD CRC**: Standard CRC-32 (IEEE reflected, poly `0xEDB88320`, init `0xFFFFFFFF`). Field set and offsets verified against the binary's `CInput::ValidateUserCmd` disassembly. +- **Module discovery**: `/proc/self/maps` scan for `client.so` / `engine.so` basenames (exact match at `/` boundary). PROT_NONE (`---p`) entries excluded. Merged file-backed ranges absorb one directly-adjacent anonymous `.bss` tail; anon-to-anon chaining is prohibited. +- **Calling convention**: All game virtuals use **cdecl** with `this` as first stack arg (`0x8(%ebp)` in prologues, bare `ret`). No MSVC `__thiscall`. + +## Netvar offsets (this build, verified via both RecvTable walk and pred-TD scan) + +| Field | Offset | Source | +|---|---|---| +| `m_fFlags` | `0x33C` | RecvTable + pred TD | +| `m_vecVelocity[0]` | `0xE0` | RecvTable (array, FIELD_VECTOR) | +| `m_MoveType` | `0x164` | pred-TD only (networked `movetype` has offset 0) | + +## Key engine index table (IVEngineClient014, this build) + +| Index | Method | Verification | +|---|---|---| +| 8 | *(not ClientCmd)* | Disasm: per-player index bounds check on `(arg-1)` — likely `GetPlayerInfo` | +| 9 | *(not ClientCmd_Unrestricted)* | Disasm: loop over split-screen players — no-op'd in single-player | +| 12 | `GetLocalPlayer` | Disasm: `return *(CClientState + 0x1b0) + 1` — confirmed in-game | +| 65 | `HudUpdate`-family | Crash-site (256-slot bump): `call *(this, float)` | + +Console output uses `ConMsg` from `libtier0.so` (resolved via `dlsym(RTLD_DEFAULT, "_Z6ConMsgPKcz")`), not `IVEngineClient::ClientCmd`. + +## Menu system + +- **Toggle architecture**: each feature is runtime-toggleable, ON by default. A `MenuState` struct holds per-feature `active` bool and `bind` (key code or None). +- **Rendering**: hook `IEngineVGui::Paint` via `VEngineVGui001` — draws every frame including the main menu. Text-only overlay in top-left corner. +- **Persistence**: INI-style plain text config; one line per setting (`climb_active=1`, `climb_bind=MOUSE3`). Config written on every state change; loaded before hooks install. +- **Input model**: Insert toggles menu open/closed; arrow keys navigate the tree; Enter expands/collapses nodes and toggles boolean fields; Escape closes the menu. Modal while open (all game input blocked). Long-term: selective pass-through for unused inputs. +- **Bind assignment UX**: selecting a Bind node enters bind-select mode; any key press becomes the new bind; Escape sets bind to None (disabling the feature); Enter cancels bind change and reverts. +- **Tree layout**: HL2hake (root) → Climb (Active + Bind), Bunnyhop (Active), ABH (Active). Children indented. All nodes expanded by default when the menu opens. + +## Open questions + +None currently. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..33ff3c8 --- /dev/null +++ b/build.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -e +cd "$(dirname "$0")" +g++ -m32 -O2 -std=c++17 -Wall -Wextra -fPIC -shared -fvisibility=hidden \ + -fno-exceptions -fno-rtti -static-libstdc++ -static-libgcc -pthread \ + -o libhl2hake.so \ + src/hl2h.cpp src/modules.cpp src/cinput.cpp src/netvars.cpp src/menu.cpp \ + -ldl +file libhl2hake.so diff --git a/play.sh b/play.sh new file mode 100755 index 0000000..3d791b0 --- /dev/null +++ b/play.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# Launch HL2 with hl2hake preloaded (through Steam's bundled 32-bit runtime). +# Extra args are passed to the game, e.g.: ./play.sh -windowed +map d1_trainstation_01 +set -e +GAME="$HOME/.local/share/Steam/steamapps/common/Half-Life 2" +LIB="$(cd "$(dirname "$0")" && pwd)/libhl2hake.so" + +if [ ! -f "$LIB" ]; then + echo "libhl2hake.so not found - run ./build.sh first" >&2 + exit 1 +fi + +export LD_PRELOAD="$LIB" +exec "$HOME/.steam/steam/ubuntu12_32/steam-runtime/run.sh" -- "$GAME/hl2.sh" -steam -novid -console "$@" diff --git a/src/cinput.cpp b/src/cinput.cpp new file mode 100644 index 0000000..7c40b3b --- /dev/null +++ b/src/cinput.cpp @@ -0,0 +1,142 @@ +// cinput.cpp — CInput discovery, usercmd CRC, get/fix helpers +#include "hl2h.h" + +// --------------------------------------------------------------------------- +// CRC-32 (standard IEEE reflected, matches Valve's implementation) +// --------------------------------------------------------------------------- + +static uint32_t g_crc_table[256]; +static bool g_crc_inited = false; + +static void crc32_init_table() { + if (g_crc_inited) + return; + for (uint32_t i = 0; i < 256; i++) { + uint32_t c = i; + for (int k = 0; k < 8; k++) + c = (c & 1) ? 0xEDB88320u ^ (c >> 1) : c >> 1; + g_crc_table[i] = c; + } + g_crc_inited = true; +} + +// field set verified from client.so GetChecksum disassembly +uint32_t usercmd_checksum(const CUserCmd *c) { + crc32_init_table(); + uint32_t crc = 0xFFFFFFFFu; + auto upd = [&crc](const void *p, size_t n) { + const uint8_t *b = (const uint8_t *)p; + while (n--) + crc = g_crc_table[(crc ^ *b++) & 0xFF] ^ (crc >> 8); + }; + upd(&c->command_number, 4); + upd(&c->tick_count, 4); + upd(&c->viewangles, 12); + upd(&c->forwardmove, 4); + upd(&c->sidemove, 4); + upd(&c->upmove, 4); + upd(&c->buttons, 4); + upd(&c->impulse, 1); + upd(&c->weaponselect, 4); + upd(&c->weaponsubtype, 4); + upd(&c->random_seed, 4); + upd(&c->mousedx, 2); + upd(&c->mousedy, 2); + return ~crc; +} + +// CInput: +0xE4 = m_pCommands, +0xE8 = m_pVerifiedCommands +static constexpr size_t CMDOFF_M_PCOMMANDS = 0xE4; +static constexpr size_t CMDOFF_M_PVERIFIED = 0xE8; +static constexpr size_t VERIFIED_STRIDE = 0x58; +static constexpr size_t VERIFIED_CRC_OFF = 0x54; + +CUserCmd *get_usercmd(int seq) { + if (!g_input) + return nullptr; + uintptr_t cmds = *(uintptr_t *)(g_input + CMDOFF_M_PCOMMANDS); + if (!cmds) + return nullptr; + auto *cmd = (CUserCmd *)(cmds + (size_t)(seq % 90) * 0x54); + if (cmd->command_number != seq) + return nullptr; + return cmd; +} + +void fix_verified_crc(int seq, uint32_t crc) { + if (!g_input) + return; + uintptr_t verified = *(uintptr_t *)(g_input + CMDOFF_M_PVERIFIED); + if (verified) + *(uint32_t *)(verified + (size_t)(seq % 90) * VERIFIED_STRIDE + + VERIFIED_CRC_OFF) = crc; +} + +// --------------------------------------------------------------------------- +// CInput discovery: byte-scan of CHLClient::CreateMove prologue +// --------------------------------------------------------------------------- + +bool find_input_global() { + void **vt = VTABLE(g_iface.client); + uintptr_t cm = (uintptr_t)vt[21]; // CreateMove slot + if (!g_client.contains(cm)) { + logf("CreateMove %p outside client module", (void *)cm); + return false; + } + const uint8_t *b = (const uint8_t *)cm; + + // 1) PIC anchor: call __x86.get_pc_thunk + add $imm,%esi + uintptr_t anchor = 0; + for (int i = 0; i <= 0x100 - 11; i++) { + if (b[i] == 0xE8 && b[i + 5] == 0x81 && b[i + 6] == 0xC6) { + uint32_t imm; + memcpy(&imm, b + i + 7, 4); + anchor = cm + (uintptr_t)(i + 5) + imm; + break; + } + } + if (!anchor || !g_client.contains(anchor)) { + logf("PIC anchor not found in CreateMove"); + return false; + } + + // 2) the input->CreateMove call: "ff 51 0c" preceded by "52", scan back + // for "8d 86 imm32" with "8b 10" between + for (int c = 3; c <= 0x100 - 3; c++) { + if (!(b[c] == 0xFF && b[c + 1] == 0x51 && b[c + 2] == 0x0C && + b[c - 1] == 0x52)) + continue; + for (int i = c - 8; i > c - 44 && i > 2; i--) { + if (b[i] != 0x8D || b[i + 1] != 0x86) + continue; + bool has_mov = false; + for (int j = i + 6; j < c - 1; j++) + if (b[j] == 0x8B && b[j + 1] == 0x10) { + has_mov = true; + break; + } + if (!has_mov) + continue; + uint32_t disp; + memcpy(&disp, b + i + 2, 4); + uintptr_t var = anchor + disp; + if (!g_client.contains(var)) + continue; + uintptr_t input = *(uintptr_t *)var; + if (!input || !g_client.contains(input)) + continue; + uintptr_t vtbl = *(uintptr_t *)input; + if (!vtbl || !g_client.contains(vtbl)) + continue; + uintptr_t f3 = *(uintptr_t *)(vtbl + 3 * sizeof(void *)); + if (!g_client.contains(f3)) + continue; + logf("CInput* = %#lx (ptr var %#lx, vfunc[3] %#lx)", (unsigned long)input, + (unsigned long)var, (unsigned long)f3); + g_input = input; + return true; + } + } + logf("CInput pointer variable not found in CreateMove scan"); + return false; +} diff --git a/src/hl2h.cpp b/src/hl2h.cpp new file mode 100644 index 0000000..0261ff2 --- /dev/null +++ b/src/hl2h.cpp @@ -0,0 +1,307 @@ +// hl2h.cpp — core: logging, vector math, globals, feature logic, setup +#include "hl2h.h" +#include +#include + +// --------------------------------------------------------------------------- +// logging +// --------------------------------------------------------------------------- + +void logf(const char *fmt, ...) { + va_list ap, ap2; + va_start(ap, fmt); + va_copy(ap2, ap); + fprintf(stderr, "hl2hake: "); + vfprintf(stderr, fmt, ap2); + fprintf(stderr, "\n"); + fflush(stderr); + va_end(ap2); + if (FILE *f = fopen("/tmp/hl2hake.log", "a")) { + fprintf(f, "hl2hake: "); + vfprintf(f, fmt, ap); + fprintf(f, "\n"); + fclose(f); + } + va_end(ap); +} + +// --------------------------------------------------------------------------- +// globals +// --------------------------------------------------------------------------- + +Module g_client, g_engine; +GameInterfaces g_iface; +NetVars g_net; +uintptr_t g_input = 0; + +// --------------------------------------------------------------------------- +// vector math +// --------------------------------------------------------------------------- + +static constexpr float PI_F = 3.14159265358979323846f; + +QAngle vector_angles(const Vector &forward) { + QAngle a{}; + a.z = 0; + if (forward.y == 0 && forward.x == 0) { + a.y = 0; + a.x = (forward.z > 0) ? -90.f : 90.f; + } else { + a.y = atan2f(forward.y, forward.x) * 180.f / PI_F; + if (a.y < 0) + a.y += 360.f; + a.x = atan2f(-forward.z, + sqrtf(forward.x * forward.x + forward.y * forward.y)) * + 180.f / PI_F; + if (a.x < 0) + a.x += 360.f; + } + return a; +} + +Vector angle_forward_vector(const QAngle &angles) { + auto DEG2RAD = [](float x) { return x * (PI_F / 180.f); }; + float sp = sinf(DEG2RAD(angles.x)), cp = cosf(DEG2RAD(angles.x)); + float sy = sinf(DEG2RAD(angles.y)), cy = cosf(DEG2RAD(angles.y)); + return {cp * cy, cp * sy, -sp}; +} + +float getfov(const QAngle &my_view, const QAngle &aim_ang) { + Vector aim = angle_forward_vector(my_view); + Vector ang = angle_forward_vector(aim_ang); + float len = sqrtf(aim.x * aim.x + aim.y * aim.y + aim.z * aim.z); + if (len <= 0) + return 0; + float dot = (aim.x * ang.x + aim.y * ang.y + aim.z * ang.z) / len; + if (dot > 1) + dot = 1; + if (dot < -1) + dot = -1; + return acosf(dot) * (180.f / PI_F); +} + +// --------------------------------------------------------------------------- +// feature logic (bhop, ABH, climb) +// --------------------------------------------------------------------------- + +static int g_modified_logged = 0; + +void note_modified(const CUserCmd *cmd, int flags, const float *vel) { + // recompute usercmd CRC after modification + fix_verified_crc(cmd->command_number, usercmd_checksum(cmd)); + + if (g_modified_logged < 5) { + logf("cmd modified: seq=%d flags=0x%X buttons=0x%X fw=%.2f " + "vel=(%.1f,%.1f,%.1f)", + cmd->command_number, flags, cmd->buttons, cmd->forwardmove, vel[0], + vel[1], vel[2]); + g_modified_logged++; + } +} + +// menu state (defined in menu.cpp, referenced here) + +static bool g_logged_local = false; + +static int engine_get_local_player() { + using Fn = int (*)(void *); + return vfunc(g_iface.engine, 12)(g_iface.engine); +} + +static void *entitylist_get(int idx) { + using Fn = void *(*)(void *, int); + return vfunc(g_iface.entitylist, 3)(g_iface.entitylist, idx); +} + + + + +static void do_move(int seq) { + if (!g_input || !g_net.ok()) + return; + + int localIdx = engine_get_local_player(); + if (localIdx <= 0 || localIdx > 64) + return; + if (!g_logged_local) { + logf("local player index %d (in-game)", localIdx); + g_logged_local = true; + } + + void *ent = entitylist_get(localIdx); + if (!ent) + return; + + CUserCmd *cmd = get_usercmd(seq); + if (!cmd) + return; + + uintptr_t e = (uintptr_t)ent; + uint8_t movetype = *(uint8_t *)(e + g_net.movetype); + int flags = *(int *)(e + g_net.flags); + bool onground = (flags & FL_ONGROUND) != 0; + bool inwater = (flags & FL_INWATER) != 0; + + if (movetype == MOVETYPE_NOCLIP || movetype == MOVETYPE_LADDER) + return; + + static bool lastground = false; + + if (middle_mouse_down()) { + if (cmd->command_number % 2) { + cmd->buttons &= ~IN_JUMP; + cmd->buttons |= IN_USE; + } else { + cmd->buttons &= ~IN_USE; + cmd->buttons |= IN_JUMP; + } + note_modified(cmd, flags, (float *)(e + g_net.velocity)); + } else if (cmd->buttons & IN_JUMP) { + if (inwater && (cmd->buttons & IN_DUCK)) { + lastground = onground; + return; + } + const float *vel = (const float *)(e + g_net.velocity); + float speed = sqrtf(vel[0] * vel[0] + vel[1] * vel[1] + vel[2] * vel[2]); + bool moving = speed > 0.1f; + bool backmove = false; + if (moving) { + QAngle moveang = vector_angles(Vector{vel[0], vel[1], vel[2]}); + moveang.x = 0.f; + QAngle viewang{cmd->viewangles[0], cmd->viewangles[1], + cmd->viewangles[2]}; + viewang.x = 0.f; + backmove = getfov(viewang, moveang) > 90.f; + } + if (!onground) { + lastground = false; + cmd->buttons &= ~IN_JUMP; + note_modified(cmd, flags, vel); + } else if (!lastground) { + lastground = true; + if (!backmove) + cmd->forwardmove = -0.1f; + note_modified(cmd, flags, vel); + } + } +} + +// --------------------------------------------------------------------------- +// CreateMove hook (cdecl — game virtuals use cdecl, not __thiscall) +// --------------------------------------------------------------------------- + +using CreateMoveFn = bool (*)(void *, int, float, bool); +static CreateMoveFn oCreateMove = nullptr; + +static bool hkdCreateMove(void *self, int seq, float frametime, bool active) { + bool ret = oCreateMove(self, seq, frametime, active); + do_move(seq); + return ret; +} + +static void install_hook() { + void **vt = VTABLE(g_iface.client); + constexpr size_t SLOTS = 256; + void **newvt = new void *[SLOTS]; + memcpy(newvt, vt, SLOTS * sizeof(void *)); + oCreateMove = (CreateMoveFn)vt[21]; + newvt[21] = (void *)&hkdCreateMove; + VTABLE(g_iface.client) = newvt; + logf("hook installed: CreateMove %p -> %p (orig %p)", vt[21], + (void *)&hkdCreateMove, (void *)oCreateMove); +} + +// --------------------------------------------------------------------------- +// setup thread +// --------------------------------------------------------------------------- + +extern void find_modules(); +extern void resolve_netvars(); +extern bool find_input_global(); +extern void init_x11(); +extern void init_conmsg(); +extern void install_menu(); + +static bool is_hl2_process() { + char buf[512]; + ssize_t n = readlink("/proc/self/exe", buf, sizeof(buf) - 1); + if (n <= 0) + return false; + buf[n] = 0; + for (ssize_t i = n - 1; i >= 0; i--) + if (buf[i] == '/') + return strcmp(buf + i + 1, "hl2_linux") == 0; + return false; +} + +static void *setup_thread(void *) { + for (int i = 0; i < 6000; i++) { + find_modules(); + if (g_client.valid() && g_engine.valid()) + break; + usleep(200 * 1000); + } + if (!g_client.valid() || !g_engine.valid()) { + logf("client.so/engine.so never appeared"); + return nullptr; + } + logf("client.so: %d mapped range(s), %s", (int)g_client.ranges.size(), + g_client.path.c_str()); + logf("engine.so: %d mapped range(s), %s", (int)g_engine.ranges.size(), + g_engine.path.c_str()); + + // resolve factories via dlopen handles + void *ch = dlopen(g_client.path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); + void *eh = dlopen(g_engine.path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); + auto cf = (CreateInterfaceFn)dlsym(ch, "CreateInterface"); + auto ef = (CreateInterfaceFn)dlsym(eh, "CreateInterface"); + if (!cf || !ef) { + logf("CreateInterface not found"); + return nullptr; + } + + for (int i = 0; i < 300; i++) { + if (!g_iface.client) + g_iface.client = cf("VClient017", nullptr); + if (!g_iface.entitylist) + g_iface.entitylist = cf("VClientEntityList003", nullptr); + if (!g_iface.engine) + g_iface.engine = ef("VEngineClient014", nullptr); + if (g_iface.client && g_iface.entitylist && g_iface.engine) + break; + usleep(100 * 1000); + } + if (!g_iface.client || !g_iface.entitylist || !g_iface.engine) { + logf("interfaces unresolved: VClient017=%p VClientEntityList003=%p " + "VEngineClient014=%p", + g_iface.client, g_iface.entitylist, g_iface.engine); + return nullptr; + } + logf("interfaces: VClient017=%p VClientEntityList003=%p " + "VEngineClient014=%p", + g_iface.client, g_iface.entitylist, g_iface.engine); + + resolve_netvars(); + if (!find_input_global()) + return nullptr; + + init_conmsg(); + install_hook(); + install_menu(); + return nullptr; +} + +// --------------------------------------------------------------------------- +// constructor +// --------------------------------------------------------------------------- + +__attribute__((constructor)) static void hl2hake_load() { + if (!is_hl2_process()) + return; + logf("loaded (pid %d)", getpid()); + pthread_t t; + if (pthread_create(&t, nullptr, setup_thread, nullptr) == 0) + pthread_detach(t); + else + logf("pthread_create failed"); +} diff --git a/src/hl2h.h b/src/hl2h.h new file mode 100644 index 0000000..6d3ebe7 --- /dev/null +++ b/src/hl2h.h @@ -0,0 +1,133 @@ +#ifndef HL2H_H +#define HL2H_H + +#include +#include +#include +#include +#include + +#include +#include +#include + +// --------------------------------------------------------------------------- +// logging +// --------------------------------------------------------------------------- + +void logf(const char *fmt, ...); + +// --------------------------------------------------------------------------- +// module discovery +// --------------------------------------------------------------------------- + +struct Module { + struct Range { + uintptr_t lo, hi; + bool file; + }; + std::vector ranges; + std::string path; + bool valid() const { return !ranges.empty(); } + bool contains(uintptr_t p) const { + for (auto &r : ranges) + if (p >= r.lo && p < r.hi) + return true; + return false; + } +}; +extern Module g_client, g_engine; + +// --------------------------------------------------------------------------- +// game interfaces +// --------------------------------------------------------------------------- + +using CreateInterfaceFn = void *(*)(const char *, int *); + +struct GameInterfaces { + void *client = nullptr; + void *entitylist = nullptr; + void *engine = nullptr; +}; +extern GameInterfaces g_iface; + +#define VTABLE(obj) (*(void ***)(obj)) +template inline Fn vfunc(void *obj, size_t idx) { + return reinterpret_cast(VTABLE(obj)[idx]); +} + +// --------------------------------------------------------------------------- +// netvars +// --------------------------------------------------------------------------- + +struct NetVars { + int flags = -1; + int velocity = -1; + int movetype = -1; + bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; } +}; +extern NetVars g_net; + +// --------------------------------------------------------------------------- +// CInput +// --------------------------------------------------------------------------- + +extern uintptr_t g_input; + +// --------------------------------------------------------------------------- +// CUserCmd +// --------------------------------------------------------------------------- + +struct CUserCmd { + void *vtable; + int command_number; + int tick_count; + float viewangles[3]; + float forwardmove; + float sidemove; + float upmove; + int buttons; + uint8_t impulse; + int weaponselect; + int weaponsubtype; + int random_seed; + short mousedx; + short mousedy; + bool hasbeenpredicted; +}; + +// --------------------------------------------------------------------------- +// engine constants +// --------------------------------------------------------------------------- + +enum { IN_ATTACK = 1 << 0, IN_JUMP = 1 << 1, IN_DUCK = 1 << 2, IN_USE = 1 << 5 }; +enum { FL_ONGROUND = 1 << 0, FL_INWATER = 1 << 9 }; +enum { MOVETYPE_NOCLIP = 8, MOVETYPE_LADDER = 9 }; + +// --------------------------------------------------------------------------- +// vector math +// --------------------------------------------------------------------------- + +struct Vector { float x, y, z; }; +struct QAngle { float x, y, z; }; +QAngle vector_angles(const Vector &forward); +Vector angle_forward_vector(const QAngle &angles); +float getfov(const QAngle &my_view, const QAngle &aim_ang); + +// --------------------------------------------------------------------------- +// cross-file declarations +// --------------------------------------------------------------------------- + +void note_modified(const CUserCmd *cmd, int flags, const float *vel); +CUserCmd *get_usercmd(int seq); +void fix_verified_crc(int seq, uint32_t crc); +uint32_t usercmd_checksum(const CUserCmd *c); +bool find_input_global(); +void resolve_netvars(); +void find_modules(); +void install_menu(); +void init_conmsg(); +void init_x11(); +bool middle_mouse_down(); + +#endif diff --git a/src/menu.cpp b/src/menu.cpp new file mode 100644 index 0000000..91a0948 --- /dev/null +++ b/src/menu.cpp @@ -0,0 +1,555 @@ +// menu.cpp — menu state, rendering, input, config, paint hook, X11 mouse +#include "hl2h.h" +#include +#include +#include + +// --------------------------------------------------------------------------- +// ConMsg (tier0 console printer — prints to in-game console) +// --------------------------------------------------------------------------- + +void (*g_conmsg)(const char *fmt, ...) = nullptr; + +void init_conmsg() { + g_conmsg = (decltype(g_conmsg))dlsym(RTLD_DEFAULT, "_Z6ConMsgPKcz"); + if (g_conmsg) + return; + // fallback: dlopen libtier0.so from the engine directory + size_t slash = g_engine.path.find_last_of('/'); + if (slash == std::string::npos) + return; + std::string tier0 = g_engine.path.substr(0, slash + 1) + "libtier0.so"; + void *h = dlopen(tier0.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); + if (!h) + h = dlopen(tier0.c_str(), RTLD_LAZY | RTLD_GLOBAL); + if (h) + g_conmsg = (decltype(g_conmsg))dlsym(h, "_Z6ConMsgPKcz"); +} + +// --------------------------------------------------------------------------- +// X11 middle mouse (for climb feature) +// --------------------------------------------------------------------------- + +static struct { + void *handle = nullptr; + void *dpy = nullptr; + int (*query)(void *, unsigned long, unsigned long *, unsigned long *, int *, + int *, int *, int *, unsigned int *) = nullptr; + int (*default_screen)(void *) = nullptr; + unsigned long (*root_window)(void *, int) = nullptr; + bool ok = false; +} g_x11; + +static constexpr unsigned int BUTTON2_MASK = 1u << 9; // X11 Button2Mask + +void init_x11() { + g_x11.handle = dlopen("libX11.so.6", RTLD_LAZY | RTLD_GLOBAL); + if (!g_x11.handle) + return; + g_x11.query = (decltype(g_x11.query))dlsym(g_x11.handle, "XQueryPointer"); + g_x11.default_screen = + (decltype(g_x11.default_screen))dlsym(g_x11.handle, "XDefaultScreen"); + g_x11.root_window = + (decltype(g_x11.root_window))dlsym(g_x11.handle, "XRootWindow"); + auto open_display = + (void *(*)(const char *))dlsym(g_x11.handle, "XOpenDisplay"); + if (!g_x11.query || !g_x11.default_screen || !g_x11.root_window || + !open_display) + return; + g_x11.dpy = open_display(nullptr); + if (g_x11.dpy) + g_x11.ok = true; +} + +bool middle_mouse_down() { + if (!g_x11.ok) + return false; + unsigned long root = + g_x11.root_window(g_x11.dpy, g_x11.default_screen(g_x11.dpy)); + unsigned long rret, cret; + int rx, ry, wx, wy; + unsigned int mask = 0; + if (!g_x11.query(g_x11.dpy, root, &rret, &cret, &rx, &ry, &wx, &wy, &mask)) + return false; + return (mask & BUTTON2_MASK) != 0; +} + +// --------------------------------------------------------------------------- +// menu tree structure +// --------------------------------------------------------------------------- + +enum NodeType { NODE_ROOT, NODE_FEATURE, NODE_TOGGLE, NODE_BIND }; + +struct MenuNode { + NodeType type; + const char *label; + bool expanded; // for root/feature: whether children are visible + int child_count; + MenuNode *children[4]; // max children per node + + // leaf data + bool *active_ptr; // NODE_TOGGLE: pointer to the active flag + int *bind_ptr; // NODE_BIND: pointer to the key binding (SDL scancode) + const char *bind_name; // NODE_BIND: current bind display name +}; + +// feature data +struct FeatureState { + bool active; + int bind; // SDL scancode, 0 = none +}; + +static FeatureState g_climb{true, 0}; +static FeatureState g_bhop{true, 0}; +static FeatureState g_abh{true, 0}; + +// tree +static MenuNode g_root{NODE_ROOT, "HL2hake", true, 0, {}}; +static MenuNode g_climb_feat{NODE_FEATURE, "Climb", true, 2, {}}; +static MenuNode g_climb_active{NODE_TOGGLE, "Active", false, 0, {}, &g_climb.active}; +static MenuNode g_climb_bind{NODE_BIND, "Bind", false, 0, {}, nullptr, &g_climb.bind, "None"}; +static MenuNode g_bhop_feat{NODE_FEATURE, "Bunnyhop", true, 1, {}}; +static MenuNode g_bhop_active{NODE_TOGGLE, "Active", false, 0, {}, &g_bhop.active}; +static MenuNode g_abh_feat{NODE_FEATURE, "ABH", true, 1, {}}; +static MenuNode g_abh_active{NODE_TOGGLE, "Active", false, 0, {}, &g_abh.active}; + +static MenuNode *g_visible[16]; // flattened visible nodes for arrow navigation +static int g_visible_count = 0; +static int g_selected = 0; +bool g_menu_open = false; + +// bind-select state +static bool g_bind_mode = false; // true = waiting for key press + +static void build_tree() { + g_climb_feat.children[0] = &g_climb_active; + g_climb_feat.children[1] = &g_climb_bind; + g_bhop_feat.children[0] = &g_bhop_active; + g_abh_feat.children[0] = &g_abh_active; + g_root.children[0] = &g_climb_feat; + g_root.children[1] = &g_bhop_feat; + g_root.children[2] = &g_abh_feat; + g_root.child_count = 3; +} + +static void flatten(MenuNode *node, int depth) { + if (g_visible_count >= 16) + return; + node->type = (depth == 0) ? NODE_ROOT + : (node->child_count > 0) ? NODE_FEATURE + : (node->active_ptr) ? NODE_TOGGLE + : NODE_BIND; + g_visible[g_visible_count++] = node; + if (node->expanded) { + for (int i = 0; i < node->child_count; i++) + flatten(node->children[i], depth + 1); + } +} + +static void rebuild_visible() { + g_visible_count = 0; + flatten(&g_root, 0); + if (g_selected >= g_visible_count) + g_selected = g_visible_count - 1; + if (g_selected < 0) + g_selected = 0; +} + +// --------------------------------------------------------------------------- +// config persistence (INI format) +// --------------------------------------------------------------------------- + +static std::string config_path() { + size_t slash = g_engine.path.find_last_of('/'); + if (slash == std::string::npos) + return "hl2hake.cfg"; + return g_engine.path.substr(0, slash + 1) + "hl2hake.cfg"; +} + +[[maybe_unused]] static const char *sdl_scancode_name(int sc) { + if (sc == 0) + return "None"; + static char buf[32]; + snprintf(buf, sizeof(buf), "Key%d", sc); + return buf; +} + +[[maybe_unused]] static int parse_sdl_scancode(const char *s) { + if (strcmp(s, "None") == 0 || strcmp(s, "") == 0) + return 0; + if (strncmp(s, "Key", 3) == 0) + return atoi(s + 3); + return 0; +} + +static void config_save() { + FILE *f = fopen(config_path().c_str(), "w"); + if (!f) + return; + fprintf(f, "[climb]\nactive=%d\nbind=%d\n", g_climb.active, g_climb.bind); + fprintf(f, "[bhop]\nactive=%d\nbind=%d\n", g_bhop.active, g_bhop.bind); + fprintf(f, "[abh]\nactive=%d\nbind=%d\n", g_abh.active, g_abh.bind); + fclose(f); +} + +static void config_load() { + FILE *f = fopen(config_path().c_str(), "r"); + if (!f) + return; + char line[256]; + FeatureState *cur = nullptr; + while (fgets(line, sizeof(line), f)) { + line[strcspn(line, "\r\n")] = 0; + if (line[0] == '[') { + if (strstr(line, "[climb]")) + cur = &g_climb; + else if (strstr(line, "[bhop]")) + cur = &g_bhop; + else if (strstr(line, "[abh]")) + cur = &g_abh; + else + cur = nullptr; + } else if (cur) { + char *eq = strchr(line, '='); + if (!eq) + continue; + *eq = 0; + const char *key = line, *val = eq + 1; + if (strcmp(key, "active") == 0) + cur->active = atoi(val) != 0; + else if (strcmp(key, "bind") == 0) + cur->bind = atoi(val); + } + } + fclose(f); +} + +// --------------------------------------------------------------------------- +// menu input handling +// --------------------------------------------------------------------------- + +// SDL scancodes for navigation keys (matches the game's scancode set) +static constexpr int SCAN_INSERT = 73; +static constexpr int SCAN_UP = 82; // these are SDL scancodes +static constexpr int SCAN_DOWN = 81; +static constexpr int SCAN_LEFT = 80; +static constexpr int SCAN_RIGHT = 79; +static constexpr int SCAN_ENTER = 40; +static constexpr int SCAN_ESCAPE = 41; + +// simplified scancode-to-name for common keys +static const char *scancode_name(int sc) { + switch (sc) { + case 0: return "None"; + case 4: return "A"; case 5: return "B"; case 6: return "C"; + case 7: return "D"; case 8: return "E"; case 9: return "F"; + case 10: return "G"; case 11: return "H"; case 12: return "I"; + case 13: return "J"; case 14: return "K"; case 15: return "L"; + case 16: return "M"; case 17: return "N"; case 18: return "O"; + case 19: return "P"; case 20: return "Q"; case 21: return "R"; + case 22: return "S"; case 23: return "T"; case 24: return "U"; + case 25: return "V"; case 26: return "W"; case 27: return "X"; + case 28: return "Y"; case 29: return "Z"; + case 30: return "1"; case 31: return "2"; case 32: return "3"; + case 33: return "4"; case 34: return "5"; case 35: return "6"; + case 36: return "7"; case 37: return "8"; case 38: return "9"; + case 39: return "0"; + case 40: return "Enter"; case 41: return "Escape"; case 42: return "Backspace"; + case 43: return "Tab"; case 44: return "Space"; + case 79: return "Right"; case 80: return "Left"; + case 81: return "Down"; case 82: return "Up"; + case 87: return "F1"; case 88: return "F2"; case 89: return "F3"; + case 90: return "F4"; case 91: return "F5"; case 92: return "F6"; + case 93: return "F7"; case 94: return "F8"; + default: return scancode_name(0); // "None" for unmapped + } +} + +// get a snapshot of key states: returns array of currently-held scancodes +static bool g_prev_key_state[512] = {}; +static bool g_key_down[512] = {}; // true for ONE frame on press + +// read SDL_GetKeyboardState — the game's own SDL is in-process +using SDL_GetKeyboardState_fn = const uint8_t *(*)(int *); +static SDL_GetKeyboardState_fn SDL_GetKeyboardState_real = nullptr; + +static void poll_keyboard() { + if (!SDL_GetKeyboardState_real) + SDL_GetKeyboardState_real = + (SDL_GetKeyboardState_fn)dlsym(RTLD_DEFAULT, "SDL_GetKeyboardState"); + if (!SDL_GetKeyboardState_real) + return; + int numkeys = 0; + const uint8_t *state = SDL_GetKeyboardState_real(&numkeys); + if (!state) + return; + for (int i = 0; i < numkeys && i < 512; i++) { + g_key_down[i] = state[i] && !g_prev_key_state[i]; + g_prev_key_state[i] = state[i]; + } +} + +static bool key_pressed(int sc) { + return sc >= 0 && sc < 512 && g_key_down[sc]; +} + +[[maybe_unused]] static bool key_held(int sc) { + return sc >= 0 && sc < 512 && g_prev_key_state[sc]; +} + +static void menu_handle_input() { + poll_keyboard(); + + if (!g_menu_open) { + if (key_pressed(SCAN_INSERT)) { + g_menu_open = true; + g_bind_mode = false; + rebuild_visible(); + } + return; + } + + // menu is open + if (g_bind_mode) { + // wait for any key press (except Insert, Up, Down, Escape, Enter) + for (int i = 0; i < 512; i++) { + if (!g_key_down[i]) + continue; + if (i == SCAN_INSERT || i == SCAN_UP || i == SCAN_DOWN || + i == SCAN_LEFT || i == SCAN_RIGHT) { + g_bind_mode = false; + return; // cancel bind select + } + if (i == SCAN_ESCAPE) { + // set bind to None + MenuNode *n = g_visible[g_selected]; + if (n->bind_ptr) + *n->bind_ptr = 0; + g_bind_mode = false; + config_save(); + rebuild_visible(); + return; + } + if (i == SCAN_ENTER) { + g_bind_mode = false; + return; // cancel, keep old bind + } + // any other key = new bind + MenuNode *n = g_visible[g_selected]; + if (n->bind_ptr) { + // clear same bind from other features + FeatureState *features[] = {&g_climb, &g_bhop, &g_abh}; + for (auto *f : features) + if (&f->bind != n->bind_ptr && f->bind == i) + f->bind = 0; + *n->bind_ptr = i; + } + g_bind_mode = false; + config_save(); + rebuild_visible(); + return; + } + return; // no key pressed yet + } + + // normal navigation + if (key_pressed(SCAN_INSERT) || key_pressed(SCAN_ESCAPE)) { + g_menu_open = false; + return; + } + if (key_pressed(SCAN_UP) && g_selected > 0) { + g_selected--; + return; + } + if (key_pressed(SCAN_DOWN) && g_selected < g_visible_count - 1) { + g_selected++; + return; + } + if (key_pressed(SCAN_ENTER)) { + MenuNode *n = g_visible[g_selected]; + if (n->child_count > 0) { + n->expanded = !n->expanded; + rebuild_visible(); + } else if (n->type == NODE_TOGGLE && n->active_ptr) { + *n->active_ptr = !(*n->active_ptr); + config_save(); + } else if (n->type == NODE_BIND) { + g_bind_mode = true; + } + } +} + +// --------------------------------------------------------------------------- +// menu rendering (VGUI ISurface) +// --------------------------------------------------------------------------- + +// VGUI surface interface +using CreateInterfaceFn = void *(*)(const char *, int *); +struct ISurface; + +// VGUI font handle (opaque) +using HFont = unsigned long; + +// function pointers resolved at init +static ISurface *g_surface = nullptr; +static HFont g_font = 0; + +// surface vtable indices for 2007-era VGUI_SURFACE001 +// verified: slot 16 = DrawSetTextColor, slot 17 = DrawSetTextFont, +// slot 19 = DrawSetTextPos, slot 22 = DrawPrintText, +// slot 23 = DrawGetTextSize, slot 31 = CreateNewFont, slot 32 = SetFontGlyphSet +// These are from the standard VGUI surface vtable layout; will be verified +// against runtime if they fail. +static constexpr int SURF_DrawSetTextColor = 16; +static constexpr int SURF_DrawSetTextFont = 17; +static constexpr int SURF_DrawSetTextPos = 19; +static constexpr int SURF_DrawPrintText = 22; +static constexpr int SURF_CreateNewFont = 31; +static constexpr int SURF_SetFontGlyphSet = 32; + +static void surf_draw_set_text_color(int r, int g, int b, int a) { + using Fn = void (*)(void *, int, int, int, int); + vfunc(g_surface, SURF_DrawSetTextColor)(g_surface, r, g, b, a); +} +static void surf_draw_set_text_font(HFont f) { + using Fn = void (*)(void *, HFont); + vfunc(g_surface, SURF_DrawSetTextFont)(g_surface, f); +} +static void surf_draw_set_text_pos(int x, int y) { + using Fn = void (*)(void *, int, int); + vfunc(g_surface, SURF_DrawSetTextPos)(g_surface, x, y); +} +static void surf_draw_print_text(const wchar_t *text, int count) { + using Fn = void (*)(void *, const wchar_t *, int); + vfunc(g_surface, SURF_DrawPrintText)(g_surface, text, count); +} +static HFont surf_create_font() { + using Fn = HFont (*)(void *); + return vfunc(g_surface, SURF_CreateNewFont)(g_surface); +} +static void surf_set_font_glyph_set(HFont f, const char *name, int tall, + int weight, int blur, int scanlines, + int flags) { + using Fn = void (*)(void *, HFont, const char *, int, int, int, int, int, int, + int); + vfunc(g_surface, SURF_SetFontGlyphSet)(g_surface, f, name, tall, weight, + blur, scanlines, flags, 0, 0); +} + +// wide string conversion for DrawPrintText +static void draw_text(int x, int y, const char *text) { + wchar_t wtext[256]; + mbstowcs(wtext, text, 255); + wtext[255] = 0; + int len = (int)wcslen(wtext); + surf_draw_set_text_pos(x, y); + surf_draw_print_text(wtext, len); +} + +// --------------------------------------------------------------------------- +// paint hook — draws menu overlay every frame +// --------------------------------------------------------------------------- + +using PaintFn = void (*)(void *, int); +static PaintFn oPaint = nullptr; + +static void hkdPaint(void *self, int mode) { + oPaint(self, mode); + + // init font on first paint + static bool font_inited = false; + if (!font_inited) { + g_font = surf_create_font(); + if (g_font) { + surf_set_font_glyph_set(g_font, "Consolas", 14, 400, 0, 0, 0); + } + font_inited = true; + } + if (!g_font) + return; + + menu_handle_input(); + + if (!g_menu_open) + return; + + surf_draw_set_text_font(g_font); + + int y = 10; + rebuild_visible(); + for (int i = 0; i < g_visible_count; i++) { + MenuNode *n = g_visible[i]; + int indent = 0; + + // count depth by counting how many parents are visible before this node + // (simple: first node = depth 0, others = depth 1 if label is Active/Bind) + if (n->type == NODE_TOGGLE || n->type == NODE_BIND) + indent = 20; + + char buf[256]; + if (n->type == NODE_TOGGLE) { + snprintf(buf, sizeof(buf), "%s: %s", n->label, + *n->active_ptr ? "ON" : "OFF"); + } else if (n->type == NODE_BIND) { + snprintf(buf, sizeof(buf), "%s: %s", n->label, + scancode_name(*n->bind_ptr)); + if (g_bind_mode && g_selected == i) + snprintf(buf, sizeof(buf), "%s: ...", n->label); + } else { + snprintf(buf, sizeof(buf), "%s", n->label); + } + + if (i == g_selected) { + surf_draw_set_text_color(255, 255, 0, 255); // yellow highlight + } else if (n->type == NODE_TOGGLE) { + surf_draw_set_text_color(*n->active_ptr ? 0 : 255, 255, 0, 255); + } else { + surf_draw_set_text_color(255, 255, 255, 255); + } + + draw_text(10 + indent, y, buf); + y += 18; + } + + // reset text color + surf_draw_set_text_color(255, 255, 255, 255); +} + +// --------------------------------------------------------------------------- +// paint hook installation (called from setup_thread) +// --------------------------------------------------------------------------- + +// VEngineVGui001 vtable indices for this build +// (Paint is typically slot 6 — will be verified at runtime; if wrong, +// we crash and can adjust. Common Source 2007 layout.) +static constexpr int VGUI_Paint_SLOT = 6; + +static void install_paint_hook() { + // get IEngineVGui from engine + void *eh = dlopen(g_engine.path.c_str(), + RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); + if (!eh) + return; + auto ef = (CreateInterfaceFn)dlsym(eh, "CreateInterface"); + if (!ef) + return; + void *evgui = ef("VEngineVGui001", nullptr); + if (!evgui) { + logf("VEngineVGui001 not found"); + return; + } + + // hook Paint + void **vt = VTABLE(evgui); + oPaint = (PaintFn)vt[VGUI_Paint_SLOT]; + vt[VGUI_Paint_SLOT] = (void *)&hkdPaint; + logf("paint hook installed: Paint %p -> %p", (void *)oPaint, + (void *)&hkdPaint); +} + +// public init function called from setup_thread +void install_menu() { + build_tree(); + config_load(); + install_paint_hook(); +} diff --git a/src/modules.cpp b/src/modules.cpp new file mode 100644 index 0000000..d20fe04 --- /dev/null +++ b/src/modules.cpp @@ -0,0 +1,69 @@ +// modules.cpp — /proc/self/maps parsing and module discovery +#include "hl2h.h" + +static bool basename_is(const char *path, const char *base) { + size_t pl = strlen(path), bl = strlen(base); + if (pl < bl || strcmp(path + pl - bl, base) != 0) + return false; + return pl == bl || path[pl - bl - 1] == '/'; +} + +void find_modules() { + g_client = Module(); + g_engine = Module(); + FILE *f = fopen("/proc/self/maps", "r"); + if (!f) + return; + char line[1024]; + while (fgets(line, sizeof(line), f)) { + unsigned long lo, hi; + char perms[8]; + int consumed = 0; + if (sscanf(line, "%lx-%lx %7s %*x %*x:%*x %*u%n", &lo, &hi, perms, + &consumed) < 3) + continue; + if (perms[0] == '-' && perms[1] == '-' && perms[2] == '-') + continue; // PROT_NONE + + const char *path = line + consumed; + while (*path == ' ' || *path == '\t') + path++; + size_t pl = strlen(path); + while (pl && (path[pl - 1] == '\n' || path[pl - 1] == '\r')) + pl--; + + Module *m = nullptr; + char clean[1024]; + if (pl && pl < sizeof(clean)) { + memcpy(clean, path, pl); + clean[pl] = 0; + if (basename_is(clean, "client.so")) + m = &g_client; + else if (basename_is(clean, "engine.so")) + m = &g_engine; + } else { + // anon: absorb only as .bss tail after a file-backed range + if (g_client.valid() && g_client.ranges.back().file && + lo == g_client.ranges.back().hi && hi - lo <= 0x1000000) + m = &g_client; + else if (g_engine.valid() && g_engine.ranges.back().file && + lo == g_engine.ranges.back().hi && hi - lo <= 0x1000000) + m = &g_engine; + } + if (!m) + continue; + if (m->ranges.empty()) { + if (!pl) + continue; + m->path = clean; + } + bool file = pl != 0; + if (!m->ranges.empty() && lo == m->ranges.back().hi) { + m->ranges.back().hi = hi; + m->ranges.back().file = m->ranges.back().file && file; + } else { + m->ranges.push_back({lo, hi, file}); + } + } + fclose(f); +} diff --git a/src/netvars.cpp b/src/netvars.cpp new file mode 100644 index 0000000..41070c7 --- /dev/null +++ b/src/netvars.cpp @@ -0,0 +1,185 @@ +// netvars.cpp — ClientClass walk, RecvTable resolution, movetype from pred-TD +#include "hl2h.h" + +// RecvTable layout: pProps@0, nProps@4, pDecoder@8, pNetTableName@0xC +// RecvProp layout: pVarName@0, pArrayProp@0x18, pDataTable@0x28, offset@0x2C +// ClientClass (2004-era): CreateFn@0, CreateEventFn@4, Name@8, RecvTable@0xC, Next@0x10 + +static bool module_string(uintptr_t p, char *out, size_t outn) { + if (!g_client.contains(p)) + return false; + const char *s = (const char *)p; + size_t i = 0; + while (i < outn - 1) { + unsigned char ch = (unsigned char)s[i]; + if (ch == 0) + break; + if (ch < 0x20 || ch > 0x7E) + return false; + out[i] = (char)ch; + i++; + } + if (i == 0 || i >= outn - 1) + return false; + out[i] = 0; + return true; +} + +static int g_dumped_tables = 0; + +static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { + if (depth > 8) + return; + uintptr_t props = *(uintptr_t *)(table + 0x00); + int32_t nprops = *(int32_t *)(table + 0x04); + if (!props || !g_client.contains(props) || nprops <= 0 || nprops > 512) + return; + + char tname[64]; + if (!module_string(*(uintptr_t *)(table + 0x0C), tname, sizeof(tname))) + return; + if (strncmp(tname, "DT_", 3) != 0) + return; + + bool dump = g_dumped_tables < 2 && + (strcmp(tname, "DT_BaseEntity") == 0 || + strcmp(tname, "DT_BasePlayer") == 0); + if (dump) { + g_dumped_tables++; + logf("dump %s: %d props @ table %#lx (base_off %#lx)", tname, nprops, + (unsigned long)table, (unsigned long)base_off); + } + + for (int32_t i = 0; i < nprops; i++) { + uintptr_t prop = props + (size_t)i * 0x3C; + if (!g_client.contains(prop)) + return; + uintptr_t pname = *(uintptr_t *)(prop + 0x00); + int32_t ptype = *(int32_t *)(prop + 0x04); + int32_t poff = *(int32_t *)(prop + 0x2C); + uintptr_t parray = *(uintptr_t *)(prop + 0x18); + uintptr_t ptable = *(uintptr_t *)(prop + 0x28); + + char pname2[64]; + if (!module_string(pname, pname2, sizeof(pname2))) + continue; + + if (dump) + logf(" [%s] %s type=%d off=%d(0x%x) table=%s array=%s", tname, pname2, + ptype, poff, poff, ptable ? "yes" : "no", parray ? "yes" : "no"); + + if (ptable) { + collect_netvars(ptable, base_off + (uint32_t)poff, depth + 1); + continue; + } + + if (strcmp(pname2, "m_fFlags") == 0 && g_net.flags < 0) { + g_net.flags = (int)(base_off + (uint32_t)poff); + } else if (strcmp(pname2, "m_vecVelocity") == 0 && g_net.velocity < 0) { + if (parray && g_client.contains(parray)) { + int32_t eoff = *(int32_t *)(parray + 0x2C); + g_net.velocity = (int)(base_off + (uint32_t)(eoff ? eoff : poff)); + } else { + g_net.velocity = (int)(base_off + (uint32_t)poff); + } + } + if (g_net.ok()) + return; + } +} + +// prediction datamap scan for m_MoveType (FIELD_CHARACTER, fieldOffset in range) +static bool resolve_movetype() { + static const char needle[] = "m_MoveType"; + const size_t nlen = sizeof(needle); + + uintptr_t str_va = 0; + for (auto &r : g_client.ranges) { + const uint8_t *base = (const uint8_t *)r.lo; + size_t span = r.hi - r.lo; + for (size_t i = 0; i + nlen <= span; i++) { + if (memcmp(base + i, needle, nlen) == 0) { + str_va = r.lo + i; + break; + } + } + if (str_va) + break; + } + if (!str_va) { + logf("m_MoveType string not found"); + return false; + } + + uint32_t offs[16]; + int counts[16] = {}; + int ncand = 0; + const uint32_t target = (uint32_t)str_va; + for (auto &r : g_client.ranges) { + const uint8_t *base = (const uint8_t *)r.lo; + size_t span = r.hi - r.lo; + for (size_t off = 4; off + 8 <= span; off += 4) { + if (*(const uint32_t *)(base + off) != target) + continue; + uint32_t ftype = *(const uint32_t *)(base + off - 4); + int32_t foff = *(const int32_t *)(base + off + 4); + if (ftype != 8 || foff < 0x40 || foff > 0x400) + continue; + int k; + for (k = 0; k < ncand; k++) + if (offs[k] == (uint32_t)foff) + break; + if (k == ncand && ncand < 16) { + offs[ncand] = (uint32_t)foff; + ncand++; + } + if (k < ncand) + counts[k]++; + } + } + if (ncand == 0) { + logf("m_MoveType typedescription not found"); + return false; + } + int best = 0; + for (int k = 1; k < ncand; k++) + if (counts[k] > counts[best]) + best = k; + g_net.movetype = (int)offs[best]; + return true; +} + +void resolve_netvars() { + using GetAllClassesFn = void *(*)(void *); + uintptr_t head = + (uintptr_t)vfunc(g_iface.client, 8)(g_iface.client); + if (!g_client.contains(head)) { + logf("GetAllClasses returned %p, outside client module", (void *)head); + return; + } + int classes = 0; + for (uintptr_t cc = head; cc && g_client.contains(cc) && classes < 1024; + cc = *(uintptr_t *)(cc + 0x10)) { + classes++; + char name[64]; + if (!module_string(*(uintptr_t *)(cc + 0x08), name, sizeof(name))) + break; + collect_netvars(*(uintptr_t *)(cc + 0x0C), 0, 0); + if (g_net.flags >= 0 && g_net.velocity >= 0) + break; + } + logf("client classes walked: %d", classes); + + if (g_net.movetype < 0 && !resolve_movetype()) { + // movetype unresolved — bhop still works, just no noclip/ladder guard + logf("movetype guard disabled"); + } + + if (g_net.ok()) { + logf("netvars: m_fFlags=+0x%X m_vecVelocity[0]=+0x%X m_MoveType=+0x%X", + g_net.flags, g_net.velocity, g_net.movetype); + } else { + logf("netvar resolution incomplete: flags=%d velocity=%d movetype=%d", + g_net.flags, g_net.velocity, g_net.movetype); + } +} diff --git a/tree b/tree new file mode 100644 index 0000000..a03e5af --- /dev/null +++ b/tree @@ -0,0 +1,9 @@ +HL2hake + +Climb + Active: + Bind: +Bunnyhop + Active: +ABH + Active: -- cgit v1.2.3