// 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, g_server; GameInterfaces g_iface; NetVars g_net; Vector g_cam_eye{0, 0, 0}; Vector g_cam_view{0, 0, 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) // --------------------------------------------------------------------------- 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; 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; } static void do_move(int seq) { 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; 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; bool bhop_on = feature_active(FEAT_BHOP); static bool lastground = false; if (bhop_on && (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; if (!inwater && bhop_autostrafe_enabled()) apply_autostrafe(cmd, vel); note_modified(cmd, flags, vel); } else if (!lastground) { lastground = true; if (!backmove) cmd->forwardmove = -0.1f; note_modified(cmd, flags, vel); } } 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)) { const float *vel = (const float *)(e + g_net.velocity); note_modified(cmd, flags, vel); } } if (feature_active(FEAT_TRIGGERBOT)) { if (triggerbot_apply(cmd)) { const float *vel = (const float *)(e + g_net.velocity); 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 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); // 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; 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_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); g_iface.modelinfo = ef("VModelInfoClient006", nullptr); g_iface.engineserver = ef("VEngineServer021", nullptr); g_iface.debugoverlay = ef("VDebugOverlay003", nullptr); g_iface.enginetrace = ef("EngineTraceClient003", 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); 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"); }