// hl2h.cpp — core: logging, vector math, globals, feature logic, setup #include "hl2h.h" #include #include #include #include #include // --------------------------------------------------------------------------- // logging // --------------------------------------------------------------------------- static FILE *g_log = nullptr; static bool g_log_debug = false; static pthread_mutex_t g_log_mu = PTHREAD_MUTEX_INITIALIZER; static void log_close() { pthread_mutex_lock(&g_log_mu); if (g_log) { fclose(g_log); g_log = nullptr; } pthread_mutex_unlock(&g_log_mu); } // Open once per process and truncate, so /tmp/hl2hake.log only ever contains // the current session. HL2H_DEBUG=1 enables logd() noise. static void log_open() { const char *dbg = getenv("HL2H_DEBUG"); g_log_debug = dbg && *dbg && strcmp(dbg, "0") != 0; g_log = fopen("/tmp/hl2hake.log", "w"); if (g_log) setvbuf(g_log, nullptr, _IOLBF, 0); atexit(log_close); } static void log_write(const char *fmt, va_list ap) { char buf[4096]; vsnprintf(buf, sizeof(buf), fmt, ap); pthread_mutex_lock(&g_log_mu); fprintf(stderr, "hl2hake: %s\n", buf); fflush(stderr); if (g_log) { fprintf(g_log, "hl2hake: %s\n", buf); fflush(g_log); } pthread_mutex_unlock(&g_log_mu); } void logf(const char *fmt, ...) { va_list ap; va_start(ap, fmt); log_write(fmt, ap); va_end(ap); } void logd(const char *fmt, ...) { if (!g_log_debug) return; va_list ap; va_start(ap, fmt); log_write(fmt, ap); va_end(ap); } // --------------------------------------------------------------------------- // globals // --------------------------------------------------------------------------- Module g_client, g_engine, g_server; GameInterfaces g_iface; NetVars g_net; Vector g_cam_eye{0, 0, 0}; Vector g_cam_view{0, 0, 0}; // --------------------------------------------------------------------------- // vector math // --------------------------------------------------------------------------- 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) // --------------------------------------------------------------------------- static int g_modified_logged = 0; // VEngineCvar004 / ICvar vtable: IAppSystem has 5 slots, then // AllocateDLLIdentifier, RegisterConCommand, UnregisterConCommand, // UnregisterConCommands, GetCommandLineValue, FindCommandBase, // FindCommandBase const, FindVar -> slot 12. // ConCommandBase has two dtor slots, then IsCommand, IsFlagSet, AddFlags, // GetName, GetHelpText, IsRegistered, GetDLLIdentifier; ConVar then adds // SetValue(const char*), SetValue(float), SetValue(int) -> slot 11. bool console_cmd(const char *cmd) { if (!cmd || !*cmd || !g_iface.engine) return false; using CmdFn = void (*)(void *, const char *); vfunc(g_iface.engine, 7)(g_iface.engine, cmd); logd("cmd: %s", cmd); return true; } bool cvar_set_int(const char *name, int value) { if (!name) return false; // Use the stable IVEngineClient ClientCmd slot rather than poking ConVar's // vtable; the installed VEngineCvar004 layout differs enough to crash. char cmd[96]; snprintf(cmd, sizeof(cmd), "%s %d", name, value); return console_cmd(cmd); } 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) { logd("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; int engine_get_local_player() { using Fn = int (*)(void *); return vfunc(g_iface.engine, 12)(g_iface.engine); } void *entitylist_get(int idx) { using Fn = void *(*)(void *, int); return vfunc(g_iface.entitylist, 3)(g_iface.entitylist, idx); } // Source air-strafe. The movement code caps the *wishspeed* used for the // acceleration delta to GetAirSpeedCap() (30) but scales accelspeed by the // full wishspeed, then clamps to the delta. Putting wishdir perpendicular to // the horizontal velocity makes the delta maximal every tick. We express that // world-space direction in the current view basis so the camera is untouched. // // The perpendicular is chosen so the velocity turns toward the direction the // player is actually asking for (W/S/A/D relative to the view), which keeps // the feature useful in every direction instead of only forward. static void apply_autostrafe(CUserCmd *cmd, const float *vel) { float vx = vel[0], vy = vel[1]; float speed = sqrtf(vx * vx + vy * vy); if (speed < 1.0f) return; float yaw = cmd->viewangles[1] * PI_F / 180.0f; float cy = cosf(yaw), sy = sinf(yaw); float fx = cy, fy = sy; // view forward on the XY plane float rx = sy, ry = -cy; // view right on the XY plane // Intended ground-plane direction from the raw movement keys (before we // overwrite them). No keys held -> keep accelerating along current travel. float tx, ty; if (fabsf(cmd->forwardmove) > 0.01f || fabsf(cmd->sidemove) > 0.01f) { tx = fx * cmd->forwardmove + rx * cmd->sidemove; ty = fy * cmd->forwardmove + ry * cmd->sidemove; float tl = sqrtf(tx * tx + ty * ty); tx /= tl; ty /= tl; } else { // No movement keys held: accelerate along the crosshair (a velocity- // relative target would keep rotating with the velocity and circle). tx = fx; ty = fy; } // +90 degrees from horizontal velocity ("left" in Source's XY convention); // the other perpendicular is its negation. Pick the side that rotates the // velocity toward the intended direction. float px = -vy / speed, py = vx / speed; float cross = vx * ty - vy * tx; // > 0 => target is left of velocity static int last_side = -1; int side = (cross >= 0.0f) ? -1 : 1; if (fabsf(cross) < 1e-6f) // exactly aligned/opposite: keep previous side side = last_side; last_side = side; float dx = (side < 0) ? px : -px; float dy = (side < 0) ? py : -py; // 450 saturates the usercmd move clamps and the engine's air-accel cap. cmd->forwardmove = (dx * fx + dy * fy) * 450.0f; cmd->sidemove = (dx * rx + dy * ry) * 450.0f; } // --------------------------------------------------------------------------- // jumpbug // --------------------------------------------------------------------------- // // Source jumpbug: while falling, hold duck; on the frame where unducking would // leave the standing hull within CategorizePosition's 2-unit ground probe, // release duck and press jump. PlayerMove calls Duck() before FullWalkMove's // CheckJumpButton, so FinishUnDuck -> CategorizePosition sets the ground // entity, the jump executes in the same frame, and CheckFalling never sees // ground for the fall-damage test. // // In air, FinishDuck raises the feet by 36 units (the standing hull is 72 // tall, the duck hull 36). We exploit that as a one-frame phase shift: press // duck when the standing distance x to the floor is about `drop` units, so // after this frame's fall the ducked feet sit ~37 units above the floor (the // middle of the [36, 38] unduck window) and the next frame can jump. static constexpr float JB_SAFE_FALL = 526.5f; static constexpr float JB_GRAVITY = 600.0f; static constexpr float JB_DUCK_RAISE = 36.0f; static constexpr float JB_TARGET_DIST = 37.0f; // middle of the unduck window static constexpr float JB_TICK = 0.015f; // Source interval_per_tick static int jb_state = 0; // 0 idle, 1 ducked/waiting for the unduck window static int jb_local = -1; static float jb_prev_z = 0.0f; static bool jb_prev_valid = false; static bool jb_prev_ducked = false; static float jb_drop = 0.0f; // last measured vertical fall per command static int jb_logs = 0; static bool jb_tracked = false; static bool game_running() { if (!g_iface.engine) return true; using BoolFn = bool (*)(void *); if (!vfunc(g_iface.engine, 26)(g_iface.engine)) return false; if (vfunc(g_iface.engine, 84)(g_iface.engine)) return false; return true; } static void jumpbug_reset() { jb_state = 0; jb_prev_valid = false; jb_tracked = false; } // Choose a vertical velocity so this command's movement ends with the feet // `target` units above the floor. The engine does StartGravity (half a tick), // moves by velocity*dt, then FinishGravity; displacement is // (vz - 0.5*g*dt)*dt. dist is the distance before the command's movement. static bool jb_force_next_distance(int local, void *ent, const float *vel, float dist, float target) { if (g_net.absvelocity < 0 || g_net.velocity < 0) return false; float dz = target - dist; float vz = dz / JB_TICK + 0.5f * JB_GRAVITY * JB_TICK; if (vz < -2000.0f) vz = -2000.0f; if (vz > 2000.0f) vz = 2000.0f; if (!server_set_velocity_z(local, vz)) return false; // Keep the local prediction in step with the authoritative server entity: // C_BaseEntity::GetAbsVelocity() reads the non-networked m_vecAbsVelocity, // while the networked m_vecVelocity is what the rest of the client sees. *(float *)((uintptr_t)ent + g_net.absvelocity + 8) = vz; ((float *)vel)[2] = vz; return true; } static bool jumpbug_apply(CUserCmd *cmd, int local, void *ent, int flags, const float *vel, float frametime) { if (!feature_active(FEAT_JUMPBUG) || !g_iface.engine || g_net.origin < 0 || g_net.velocity < 0) { jumpbug_reset(); return false; } if (!game_running()) { jumpbug_reset(); return false; } bool onground = (flags & FL_ONGROUND) != 0; bool inwater = (flags & FL_INWATER) != 0; bool ducked = (flags & FL_DUCKING) != 0; uintptr_t e = (uintptr_t)ent; int movetype = g_net.movetype >= 0 ? *(int *)(e + g_net.movetype) : MOVETYPE_WALK; if (onground || inwater || movetype != MOVETYPE_WALK || (g_net.health >= 0 && *(int *)(e + g_net.health) <= 0)) { jumpbug_reset(); return false; } Vector origin{*(float *)(e + g_net.origin), *(float *)(e + g_net.origin + 4), *(float *)(e + g_net.origin + 8)}; float vz = vel[2]; if (local != jb_local) { jb_local = local; jumpbug_reset(); jb_drop = 0.0f; jb_logs = 0; } // Measure the previous command's fall. Both endpoints must be airborne and // standing: a duck transition moves the origin by 36 units and would // otherwise look like a huge fall. if (jb_prev_valid && !jb_prev_ducked && !ducked) { float d = jb_prev_z - origin.z; if (d > 0.05f && d < 250.0f) jb_drop = d; } jb_prev_valid = true; jb_prev_z = origin.z; jb_prev_ducked = ducked; if (vz > -10.0f) { // not falling jb_state = 0; jb_tracked = false; return false; } float x = ground_distance(origin, ent, 2048.0f); if (x < 0.0f) { // no standable surface below jb_state = 0; jb_tracked = false; return false; } float impact = sqrtf(vz * vz + 2.0f * JB_GRAVITY * x); if (impact <= JB_SAFE_FALL + 0.5f) { jb_tracked = false; jb_state = 0; return false; } if (!jb_tracked) { jb_tracked = true; logd("jumpbug: tracking x=%.1f vz=%.1f impact=%.1f", x, vz, impact); } bool assist = jumpbug_assist_enabled(); if (assist) resolve_server_netvars(); if (jb_drop <= 0.05f) { // No measured fall yet. Bias toward the default 15 ms Source tick when // the input frametime is smaller: setting the duck up early leaves us // ducked above the window, where we can wait; setting it up late would // put us below the unduck window and lose the jump. float dt = frametime; if (!(dt > 0.0f && dt < 0.05f) || dt < 0.015f) dt = 0.015f; jb_drop = -vz * dt + 0.5f * JB_GRAVITY * dt * dt; if (jb_drop < 0.05f) jb_drop = 0.05f; } int before = cmd->buttons; bool trigger = false; if (ducked) { if (x >= JB_DUCK_RAISE && x <= JB_DUCK_RAISE + 2.0f) { // In the window: unduck (CategorizePosition sets ground) and jump in // the same command. cmd->buttons &= ~IN_DUCK; cmd->buttons |= IN_JUMP; trigger = true; jb_state = 0; jb_tracked = false; if (jb_logs < 30) { jb_logs++; logd("jumpbug: trigger x=%.2f drop=%.2f vz=%.1f assist=%d", x, jb_drop, vz, (int)assist); } } else { if (assist) { float next_x = x - jb_drop; if (next_x <= 38.0f && next_x < 36.0f) { // The natural next command would skip the 2-unit window. Force the // post-command distance to 37 so the next command can unduck+jump. if (jb_force_next_distance(local, ent, vel, x, JB_TARGET_DIST)) { if (jb_logs < 30) { jb_logs++; logd("jumpbug: assist duck x=%.2f next=%.2f -> vz=%.1f", x, next_x, vel[2]); } } else { static bool warned = false; if (!warned) { warned = true; logf("jumpbug: assist unavailable (server velocity failed)"); } } } } // Hold the crouch and wait for the window (or for the assisted // position on the next command). cmd->buttons |= IN_DUCK; jb_state = 1; } } else { if (jb_state == 1) jb_state = 0; // expected to be ducked but is not float setup_limit = jb_drop + (assist ? 4.0f : 2.0f); if (x <= setup_limit) { // Setup frame: raise the feet 36 units here so the next command starts // near the middle of the unduck window. With assist enabled, also pin // the post-command distance to exactly 37. bool forced = false; if (assist) forced = jb_force_next_distance(local, ent, vel, x + JB_DUCK_RAISE, JB_TARGET_DIST); cmd->buttons |= IN_DUCK; jb_state = 1; if (jb_logs < 30) { jb_logs++; if (assist) logd("jumpbug: setup assist x=%.2f drop=%.2f vz=%.1f ok=%d", x, jb_drop, vel[2], (int)forced); else logd("jumpbug: setup x=%.2f drop=%.2f pred=%.2f vz=%.1f", x, jb_drop, x + JB_DUCK_RAISE - jb_drop, vz); } } } if (!trigger && (cmd->buttons & IN_JUMP)) cmd->buttons &= ~IN_JUMP; // keep the pogo latch clear for the trigger return cmd->buttons != before; } // --------------------------------------------------------------------------- // server RandomSeed diagnostic / override // --------------------------------------------------------------------------- // // The server is a stripped build whose usercmd/random-seed handling differs // from the 2013 SDK source. Hook the server.so PLT slot for RandomSeed so we // can force the chosen spread seed. The GOT offset is read from the loaded // module's relocation table at runtime (module_jump_slot): hardcoding it broke // when Steam switched to the hl2_complete/server.so branch (0xce1348 -> // 0xce5348). static void (*g_real_random_seed)(int) = nullptr; static uintptr_t g_srv_base = 0; static uintptr_t g_local_server_player = 0; static bool g_seed_hook_installed = false; // Pellet-spread window armed by the player's FireBullets RandomSeed call. static volatile int g_spread_rand_left = 0; static void hkdSrvRandomSeed(int seed) { uintptr_t ret = (uintptr_t)__builtin_return_address(0); long off = (long)(ret - g_srv_base); static bool trace = getenv("HL2H_SEED_TRACE") != nullptr; static int logs = 0; // The caller's first argument is `this`. Only FireBullets is called on the // player entity; the CUniformRandomStream wrappers and the constructor-time // RandomSeed(0) reset have a different object there. void *frame = nullptr; __asm__ volatile("movl %%ebp, %0" : "=r"(frame)); if (frame) frame = *(void **)frame; // caller's ebp (CBaseEntity::FireBullets) void *self = frame ? *(void **)((char *)frame + 8) : nullptr; bool local = self && g_local_server_player && (uintptr_t)self == g_local_server_player; if (local && g_server.contains(ret) && feature_active(FEAT_SPREAD) && spread_no_spread_enabled() && spread_engine_enabled()) { g_spread_rand_left = 8; int pellets = g_spread_pellets; if (pellets < 1) pellets = 1; if (pellets > 12) pellets = 12; static int idx = 0; static double last = 0.0; struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); double now = (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; if (now - last > 0.05 || idx >= pellets) idx = 0; last = now; seed = spread_best_seed(pellets) + idx; idx++; } if (trace && logs < 200) { logs++; logd("srvseed: seed=%d off=0x%lx self=%p local=%d", seed, off, self, (int)local); } if (g_real_random_seed) g_real_random_seed(seed); } static void install_seed_hook() { static bool failed = false; if (g_seed_hook_installed || failed || !g_server.valid()) return; g_srv_base = g_server.ranges[0].lo; uintptr_t reloc = 0, bias = 0; uintptr_t slot = 0; if (module_jump_slot(g_server.path.c_str(), "RandomSeed", &reloc, &bias)) { slot = g_srv_base + reloc - bias; logf("seed hook: RandomSeed GOT reloc=%#lx bias=%#lx -> %#lx", (unsigned long)reloc, (unsigned long)bias, (unsigned long)slot); } else { // ELF parse failed (deleted/unreadable module); try the builds we know. static const uintptr_t known[] = {0xce5348, 0xce1348}; for (size_t i = 0; i < sizeof(known) / sizeof(known[0]) && !slot; i++) { uintptr_t cand = g_srv_base + known[i]; if (g_server.contains(cand)) slot = cand; } logf("seed hook: ELF reloc scan failed, fallback slot=%#lx", (unsigned long)slot); } if (!slot || !g_server.contains(slot)) { failed = true; logf("seed hook: no writable RandomSeed slot"); return; } g_real_random_seed = (void (*)(int))dlsym(RTLD_NEXT, "RandomSeed"); if (!g_real_random_seed) g_real_random_seed = (void (*)(int))dlsym(RTLD_DEFAULT, "RandomSeed"); if (!g_real_random_seed) { failed = true; logf("seed hook: real RandomSeed not found"); toastf(TOAST_WARN, "Spread seed hook unavailable"); return; } mprotect((void *)(slot & ~(uintptr_t)0xFFF), 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC); *(uint32_t *)slot = (uint32_t)(uintptr_t)&hkdSrvRandomSeed; g_seed_hook_installed = true; logf("seed hook: server GOT %#lx -> %p (real %p)", (unsigned long)slot, (void *)&hkdSrvRandomSeed, (void *)g_real_random_seed); } // --------------------------------------------------------------------------- // pellet spread: zero the random offsets inside FireBullets // --------------------------------------------------------------------------- // // CShotManipulator::ApplySpread is inlined into CBaseEntity::FireBullets and // draws its x/y offsets through the global CUniformRandomStream (vtable slot // 1, confirmed against libvstdlib: _ZTV20CUniformRandomStream + 8 + 4). The // seed search can only choose the least-bad sequence, which still leaves the // shotgun's pellets slightly apart. Returning 0 for these draws makes every // pellet follow the shot direction exactly. The hook is conditional on the // calling frame belonging to the local server player, so NPC fire and the // client's own prediction stream keep their randomness. static float (*g_orig_uniform_float)(void *, float, float) = nullptr; static bool g_uniform_hook_installed = false; // g_spread_rand_left is set for a few draws right after the player's // FireBullets RandomSeed. The ApplySpread frame is not always the player's // (the compiler may outline it), so this window is the reliable signal. static float hkdUniformRandomFloat(void *self, float mn, float mx) { bool enabled = g_local_server_player && feature_active(FEAT_SPREAD) && spread_no_spread_enabled() && spread_engine_enabled(); bool hit = false; void *who = nullptr; if (enabled) { void *frame = nullptr; __asm__ volatile("movl %%ebp, %0" : "=r"(frame)); if (frame) { frame = *(void **)frame; // caller's ebp who = frame ? *(void **)((char *)frame + 8) : nullptr; if (who && (uintptr_t)who == g_local_server_player) hit = true; } if (!hit && g_spread_rand_left > 0) { // The next few draws after the player's FireBullets RandomSeed are the // pellet spread, regardless of which function emits them. hit = true; g_spread_rand_left--; } } if (hit) return 0.0f; return g_orig_uniform_float ? g_orig_uniform_float(self, mn, mx) : mn; } static void install_uniform_random_hook() { if (g_uniform_hook_installed) return; void *vt = dlsym(RTLD_DEFAULT, "_ZTV20CUniformRandomStream"); if (!vt) { logf("spread: CUniformRandomStream vtable not found"); return; } void **ap = (void **)((char *)vt + 8); // skip offset-to-top and typeinfo void *rf = dlsym(RTLD_DEFAULT, "_ZN20CUniformRandomStream11RandomFloatEff"); int slot = 1; // RandomFloat(min,max) follows SetSeed in the vtable if (rf) { for (int i = 0; i < 8; i++) { if (ap[i] == rf) { slot = i; break; } } } g_orig_uniform_float = (float (*)(void *, float, float))ap[slot]; uintptr_t page = (uintptr_t)&ap[slot] & ~(uintptr_t)0xFFF; if (mprotect((void *)page, 0x1000, PROT_READ | PROT_WRITE) != 0) { logf("spread: could not make the random vtable writable"); return; } ap[slot] = (void *)&hkdUniformRandomFloat; g_uniform_hook_installed = true; logf("spread: CUniformRandomStream::RandomFloat slot %d (%p) -> %p", slot, (void *)g_orig_uniform_float, (void *)&hkdUniformRandomFloat); } // --------------------------------------------------------------------------- // camera / viewmodel FOV and gamma // --------------------------------------------------------------------------- // // The rendered camera FOV is derived from C_BasePlayer::m_iDefaultFOV every // frame (CViewRender::OnRenderStart copies it into default_fov), so writing // the client netvar is enough and needs no server command. The viewmodel FOV // and monitor gamma are plain client cvars. static void view_settings_apply(void *ent) { if (g_net.default_fov >= 0 && ent) { static int last = -2; int want = g_fov_camera > 0 ? g_fov_camera : 0; if (want != last || g_fov_camera > 0) { *(int *)((uintptr_t)ent + g_net.default_fov) = want; last = want; } } // 0 means "leave the game's value alone": only send a command once the // slider is non-zero, then restore the stock value when it returns to 0. static int last_vm = 0; if (g_fov_viewmodel > 0 && g_fov_viewmodel != last_vm) { char cmd[48]; snprintf(cmd, sizeof(cmd), "viewmodel_fov %d", g_fov_viewmodel); console_cmd(cmd); last_vm = g_fov_viewmodel; } else if (g_fov_viewmodel == 0 && last_vm > 0) { console_cmd("viewmodel_fov 54"); last_vm = 0; } static int last_gamma = 22; if (g_gamma != last_gamma) { char cmd[48]; snprintf(cmd, sizeof(cmd), "mat_monitorgamma %d.%d", g_gamma / 10, g_gamma % 10); console_cmd(cmd); last_gamma = g_gamma; } } // --------------------------------------------------------------------------- // jump assist: crouch-jump and edge jump // --------------------------------------------------------------------------- static bool g_cj_added = false; static bool crouch_jump_apply(CUserCmd *cmd, int flags, const float *vel) { bool user_duck = (cmd->buttons & IN_DUCK) != 0; bool onground = (flags & FL_ONGROUND) != 0; bool want = g_auto_crouchjump && !onground && !user_duck && (cmd->buttons & IN_JUMP) && vel[2] > 20.0f; int old = cmd->buttons; if (want) { cmd->buttons |= IN_DUCK; g_cj_added = true; } else if (g_cj_added && !user_duck) { cmd->buttons &= ~IN_DUCK; g_cj_added = false; } return cmd->buttons != old; } static bool edge_jump_apply(CUserCmd *cmd, void *ent, int flags, const float *vel) { if (!g_edge_jump || g_net.origin < 0 || !(flags & FL_ONGROUND) || (flags & FL_INWATER)) return false; if (cmd->buttons & (IN_JUMP | IN_DUCK)) return false; float speed2d = sqrtf(vel[0] * vel[0] + vel[1] * vel[1]); if (speed2d < 120.0f) return false; uintptr_t e = (uintptr_t)ent; Vector origin{*(float *)(e + g_net.origin), *(float *)(e + g_net.origin + 4), *(float *)(e + g_net.origin + 8)}; // Probe the ground a fraction of a second ahead. No hit within 64 units // means a real ledge; slopes and stairs still find a floor and are skipped. Vector ahead{origin.x + vel[0] * 0.12f, origin.y + vel[1] * 0.12f, origin.z}; if (!g_iface.enginetrace || point_ground(ahead, ent)) return false; cmd->buttons |= IN_JUMP; return true; } static void do_move(int seq, float frametime) { install_seed_hook(); install_uniform_random_hook(); static bool g_echo_sent = false; if (!g_input || !g_net.ok()) return; if (!g_echo_sent) { g_echo_sent = true; if (g_conmsg) { g_conmsg("[hl2hake] succesfully loaded\n"); logf("console echo sent (first CreateMove)"); } else { logf("g_conmsg is NULL — in-game console output unavailable"); } } 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; view_settings_apply(ent); // New map / respawn: drop stale bone caches and target locks. static void *last_local_ent = nullptr; if (ent != last_local_ent) { last_local_ent = ent; bones_clear(); aimbot_reset_lock(); silentaim_reset_lock(); fullbright_invalidate(); world_tint_invalidate(); fog_invalidate(); skybox_invalidate(); } if (feature_active(FEAT_SPREAD)) { resolve_server_netvars(); void *srv = nullptr; if (server_entity_health(localIdx, nullptr, nullptr, &srv) >= 0 && srv) g_local_server_player = (uintptr_t)srv; } CUserCmd *cmd = get_usercmd(seq); if (!cmd) return; uintptr_t e = (uintptr_t)ent; const float *vo = (const float *)(e + g_net.viewoffset); g_cam_eye = {*(float *)(e + g_net.origin) + vo[0], *(float *)(e + g_net.origin + 4) + vo[1], *(float *)(e + g_net.origin + 8) + vo[2]}; g_cam_view = {cmd->viewangles[0], cmd->viewangles[1], cmd->viewangles[2]}; int flags = *(int *)(e + g_net.flags); bool onground = (flags & FL_ONGROUND) != 0; bool inwater = (flags & FL_INWATER) != 0; const float *vel = (const float *)(e + g_net.velocity); if (gui_is_open()) { QAngle f = gui_frozen_view(); g_cam_view = Vector{f.x, f.y, f.z}; cmd->viewangles[0] = f.x; cmd->viewangles[1] = f.y; cmd->viewangles[2] = f.z; // Freeze the rendered view explicitly: CInput may not push the modified // command view back to the engine if the client mode returns false. if (g_iface.engine) { QAngle set = f; using SetViewFn = void (*)(void *, QAngle *); vfunc(g_iface.engine, 20)(g_iface.engine, &set); } if (gui_bind_capture_active()) { // Bind capture must not move/act in the game while keys are sampled. cmd->forwardmove = 0.0f; cmd->sidemove = 0.0f; cmd->upmove = 0.0f; cmd->buttons = 0; } else { // Movement is allowed while the GUI is open, but not look/attack/use. // Keep locomotion buttons (jump, duck, walk) only. cmd->buttons &= (IN_JUMP | IN_DUCK | IN_SPEED); } cmd->impulse = 0; cmd->weaponselect = 0; cmd->weaponsubtype = 0; cmd->mousedx = 0; cmd->mousedy = 0; note_modified(cmd, flags, vel); return; } bool bhop_on = feature_active(FEAT_BHOP); bool abh_on = feature_active(FEAT_ABH); static bool lastground = false; if ((bhop_on || abh_on) && (cmd->buttons & IN_JUMP)) { if (inwater && (cmd->buttons & IN_DUCK)) { lastground = onground; return; } 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; if (bhop_on && !inwater && bhop_autostrafe_enabled()) apply_autostrafe(cmd, vel); note_modified(cmd, flags, vel); } else if (!lastground) { lastground = true; if (abh_on) { // ABH jump tick: face opposite the horizontal velocity, hold no // directional keys. The jump speed-bound subtraction then flips into // acceleration along the velocity. The rendered view is restored at // FRAME_RENDER_START so the camera does not spin around. float speed2d = sqrtf(vel[0] * vel[0] + vel[1] * vel[1]); if (speed2d > 1.0f) { float vyaw = atan2f(vel[1], vel[0]) * 180.0f / PI_F; float fake = vyaw + 180.0f; while (fake > 180.0f) fake -= 360.0f; while (fake < -180.0f) fake += 360.0f; cmd->viewangles[1] = fake; cmd->forwardmove = 0.0f; cmd->sidemove = 0.0f; set_silent_restore(QAngle{g_cam_view.x, g_cam_view.y, g_cam_view.z}); } } else if (!backmove) { cmd->forwardmove = -0.1f; } note_modified(cmd, flags, vel); } } bool jump_assist = crouch_jump_apply(cmd, flags, vel); if (edge_jump_apply(cmd, ent, flags, vel)) jump_assist = true; if (jump_assist) note_modified(cmd, flags, vel); if (jumpbug_apply(cmd, localIdx, ent, flags, vel, frametime)) note_modified(cmd, flags, vel); bool melee = feature_active(FEAT_MELEE) && melee_weapon_active(ent); if (!feature_active(FEAT_AIMBOT) || !feature_has_bind(FEAT_AIMBOT) || melee) aimbot_reset_lock(); if (!feature_active(FEAT_SILENTAIM) || !feature_has_bind(FEAT_SILENTAIM) || melee) silentaim_reset_lock(); if (melee) { if (melee_aimbot_apply(cmd)) note_modified(cmd, flags, vel); } else { if (feature_active(FEAT_AIMBOT)) { static bool aim_feature_logged = false; if (!aim_feature_logged) { aim_feature_logged = true; logf("aimbot: feature active (bind held)"); } if (aimbot_apply(cmd)) note_modified(cmd, flags, vel); } if (feature_active(FEAT_TRIGGERBOT)) { if (triggerbot_apply(cmd)) { // Triggerbot sets IN_ATTACK after CInput's spread hook, so the best // seed has to be applied here as well. spread_control_apply(cmd, ent); note_modified(cmd, flags, vel); } } if (feature_active(FEAT_SILENTAIM)) { if (silent_aim_apply(cmd)) { // Silent aim also sets IN_ATTACK late; without this the shot keeps // the random CInput seed instead of the minimal-spread seed. spread_control_apply(cmd, ent); 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; using FrameStageFn = void (*)(void *, int); static FrameStageFn oFrameStageNotify = nullptr; static QAngle g_silent_restore_angles{}; static bool g_silent_restore_pending = false; void set_silent_restore(const QAngle &angles) { g_silent_restore_angles = angles; g_silent_restore_pending = true; } // FRAME_RENDER_START (ClientFrameStage_t: -1 undefined, 0 START, ..., // 4 NET_UPDATE_END, 5 RENDER_START). static void hkdFrameStageNotify(void *self, int stage) { if (oFrameStageNotify) oFrameStageNotify(self, stage); if (stage == 5) { // Skybox texture uploads happen here, not in the VGUI paint hook: the // material system only downloads procedural textures on a render-safe // frame stage. skybox_update(); if (g_silent_restore_pending) { g_silent_restore_pending = false; if (g_iface.engine) { QAngle a = g_silent_restore_angles; using SetViewFn = void (*)(void *, QAngle *); vfunc(g_iface.engine, 20)(g_iface.engine, &a); } } if (gui_is_open() && g_iface.engine) { QAngle f = gui_frozen_view(); using SetViewFn = void (*)(void *, QAngle *); vfunc(g_iface.engine, 20)(g_iface.engine, &f); } } } using DrawPrintTextFn = void (*)(void *self, const wchar_t *text, int len, int drawType); static DrawPrintTextFn oDrawPrintText = nullptr; using PaintTraverseExFn = void (*)(void *, long, bool); static PaintTraverseExFn oPaintTraverseEx = nullptr; static bool g_in_draw = false; // Hooked ISurface slot 22. The game calls this from a valid paint context; // our overlay draws are submitted through outer_draw() (menu.cpp), which // wraps them in PushMakeCurrent/PopMakeCurrent with the embedded panel. static void hkdDrawPrintText(void *self, const wchar_t *text, int len, int drawType) { if (g_in_draw) { oDrawPrintText(self, text, len, drawType); return; } g_in_draw = true; oDrawPrintText(self, text, len, drawType); // fallback trigger: fires when the game draws text (menus). Primary draw // path is the PaintTraverseEx hook (slot 114), which fires every frame. outer_draw(); g_in_draw = false; } // PaintTraverseEx (slot 114, verified: IsVisible check + popups loop + "Too many // popups" assert). Fires every frame for the root panel — unlike DrawPrintText // (slot 22) which only fires when text is drawn, so it never fires in-world. static void hkdPaintTraverseEx(void *self, long panel, bool paintPopups) { using Fn = void (*)(void *, long, bool); ((Fn)oPaintTraverseEx)(self, panel, paintPopups); if (!g_in_draw) { g_in_draw = true; outer_draw(); g_in_draw = false; } } static bool hkdCreateMove(void *self, int seq, float frametime, bool active) { bool ret = oCreateMove(self, seq, frametime, active); do_move(seq, frametime); // Hook ISurface once: PaintTraverseEx (slot 114) fires every frame; keep // DrawPrintText (slot 22) hooked as a fallback text-draw trigger. static int isurface_hooked = 0; if (!isurface_hooked && g_surface) { void **svt = VTABLE(g_surface); constexpr size_t VT_SIZE = 256; void **newsvt = new void *[VT_SIZE]; memcpy(newsvt, svt, VT_SIZE * sizeof(void *)); oDrawPrintText = (DrawPrintTextFn)svt[22]; newsvt[22] = (void *)&hkdDrawPrintText; oPaintTraverseEx = (PaintTraverseExFn)svt[114]; newsvt[114] = (void *)&hkdPaintTraverseEx; VTABLE(g_surface) = newsvt; isurface_hooked = 1; logf("ISurface hooked: DrawPrintText slot 22 orig=%p, PaintTraverseEx slot 114 orig=%p", (void *)oDrawPrintText, (void *)oPaintTraverseEx); } 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; oFrameStageNotify = (FrameStageFn)vt[35]; newvt[35] = (void *)&hkdFrameStageNotify; VTABLE(g_iface.client) = newvt; logf("hook installed: CreateMove %p -> %p (orig %p), FrameStageNotify %p " "-> %p (orig %p)", vt[21], (void *)&hkdCreateMove, (void *)oCreateMove, vt[35], (void *)&hkdFrameStageNotify, (void *)oFrameStageNotify); } // --------------------------------------------------------------------------- // setup thread // --------------------------------------------------------------------------- extern void find_modules(); extern void resolve_netvars(); extern bool find_input_global(); 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"); toastf(TOAST_ERROR, "client.so/engine.so not found; hl2hake inactive"); 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"); toastf(TOAST_ERROR, "CreateInterface not found; hl2hake inactive"); 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); toastf(TOAST_ERROR, "Game interfaces unresolved; hl2hake inactive"); return nullptr; } logf("interfaces: VClient017=%p VClientEntityList003=%p " "VEngineClient014=%p", g_iface.client, g_iface.entitylist, g_iface.engine); g_iface.modelinfo = ef("VModelInfoClient006", nullptr); g_iface.engineserver = ef("VEngineServer021", nullptr); g_iface.debugoverlay = ef("VDebugOverlay003", nullptr); g_iface.enginetrace = ef("EngineTraceClient003", nullptr); static const char *cvar_names[] = {"VEngineCvar004", "VEngineCvar007", "VEngineCvar003", "IVEngineCvar004", nullptr}; for (int i = 0; cvar_names[i] && !g_iface.cvar; i++) { g_iface.cvar = ef(cvar_names[i], nullptr); if (!g_iface.cvar) g_iface.cvar = cf(cvar_names[i], nullptr); } if (!g_iface.cvar) { size_t slash = g_engine.path.find_last_of('/'); if (slash != std::string::npos) { std::string path = g_engine.path.substr(0, slash + 1) + "libvstdlib.so"; void *h = dlopen(path.c_str(), RTLD_NOLOAD | RTLD_LAZY | RTLD_GLOBAL); if (!h) h = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL); if (h) { auto vf = (CreateInterfaceFn)dlsym(h, "CreateInterface"); if (vf) { for (int i = 0; cvar_names[i] && !g_iface.cvar; i++) g_iface.cvar = vf(cvar_names[i], nullptr); } } } } logf("modelinfo: %p (VModelInfoClient006)", g_iface.modelinfo); logf("engineserver: %p (VEngineServer021), debugoverlay: %p, " "enginetrace: %p (EngineTraceClient003)", g_iface.engineserver, g_iface.debugoverlay, g_iface.enginetrace); logf("cvar: %p (VEngineCvar004)", g_iface.cvar); resolve_netvars(); if (!g_net.ok()) toastf(TOAST_ERROR, "Netvars incomplete; features may be disabled"); else if (g_net.absvelocity < 0) toastf(TOAST_WARN, "Jumpbug assist disabled: velocity netvar missing"); if (!g_iface.engineserver) toastf(TOAST_WARN, "Server interfaces missing; jumpbug/spread limited"); if (!g_iface.debugoverlay) toastf(TOAST_WARN, "Debug overlay missing; ESP projection uses fallback"); if (!g_iface.enginetrace) toastf(TOAST_WARN, "Engine trace missing; vischeck fails open"); if (!find_input_global()) { toastf(TOAST_ERROR, "Input hook failed; movement features disabled"); return nullptr; } init_conmsg(); install_hook(); install_menu(); if (g_surface) toastf(TOAST_INFO, "hl2hake loaded - Insert opens the menu"); return nullptr; } // --------------------------------------------------------------------------- // constructor // --------------------------------------------------------------------------- __attribute__((constructor)) static void hl2hake_load() { if (!is_hl2_process()) return; log_open(); logf("loaded (pid %d) build %s %s", getpid(), __DATE__, __TIME__); pthread_t t; if (pthread_create(&t, nullptr, setup_thread, nullptr) == 0) pthread_detach(t); else logf("pthread_create failed"); }