From bb99c978c5f982e80a3007101fd61fe23635e242 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 27 Oct 2026 13:19:00 +0100 Subject: Jumpbug: automatic duck+jump on damaging falls - Predict the landing speed from the current vertical velocity and a swept standing-hull ground trace; engage only when it exceeds PLAYER_MAX_SAFE_FALL_SPEED. - Press duck one command before the unduck window so FinishDuck's 36-unit origin raise places the feet ~37 units above the floor. - On the next command release duck and press jump: PlayerMove runs Duck() before CheckJumpButton, so FinishUnDuck sets ground, the jump executes, and CheckFalling never sees ground for the fall-damage check. - Measure the per-command fall from consecutive airborne origins instead of a fixed tick interval; clear IN_JUMP while waiting so the pogo latch is clear. - New Movement -> Jumpbug -> Active toggle (default on), config [jumpbug]. --- CONTEXT.md | 30 ++++++++- src/hl2h.cpp | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- src/hl2h.h | 8 ++- src/menu.cpp | 28 ++++++--- src/vischeck.cpp | 45 ++++++++++++++ 5 files changed, 279 insertions(+), 18 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index cd3811d..b1ec4e7 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -104,21 +104,26 @@ HL2hake Uni-directional auto-strafe ABH Active + Jumpbug + Active ESP Active - Show neutral / healthbar / healthnum / name / skeleton / box + Show neutral / healthbar / healthnum / name / skeleton / box / speed graph Aimbot Active Bind Vischeck Bones -> Head / Torso / Left arm / Right arm / Left leg / Right leg Smoothness (1..45 deg/tick, Left/Right) + FOV (1..180 deg, Left/Right) + Show FOV circle Triggerbot Active Bind Silent aim Active Bind + FOV (1..180 deg, Left/Right) ``` Binds use SDL scancodes; mouse buttons are pseudo-scancodes 600..604. @@ -156,6 +161,27 @@ Binds use SDL scancodes; mouse buttons are pseudo-scancodes 600..604. zero forward/side movement; HL2's jump speed-bound subtraction then accelerates along the velocity. Camera restored at `FRAME_RENDER_START`. +## Jumpbug + +- Automatic while falling when the predicted landing speed + `sqrt(vz^2 + 2*600*x)` exceeds `PLAYER_MAX_SAFE_FALL_SPEED` (526.5). +- The engine only applies fall damage when `CheckFalling` sees a ground entity + at the end of the frame. `PlayerMove` runs `Duck()` **before** + `FullWalkMove`'s `CheckJumpButton`, so a same-frame `FinishUnDuck` sets ground + via `CategorizePosition`, the jump executes, and the fall-damage test is + skipped. +- In air, `FinishDuck` raises the origin by 36 (standing hull 72, duck hull 36) + and `CanUnduck` only succeeds when the floor is at least 36 below; after + unducking, `CategorizePosition` needs it within 2 more units. The unduck + window is therefore a 2-unit band `[36,38]` above the floor. +- The feature presses duck when the standing distance `x` is within + `drop + [0,2]` (where `drop` is the previous command's measured fall), then + releases duck + `IN_JUMP` on the next command when `x` is in `[36,38]`. + It also clears `IN_JUMP` while waiting so the pogo latch is clear. +- `drop` is measured from consecutive predicted origins (not a hard-coded tick + interval). At high fall speeds the 2-unit band can be skipped between + commands; a higher frame rate improves reliability. Best-effort by design. + ## Gotchas / lessons - Never pipe-mask build failures: @@ -171,8 +197,6 @@ Binds use SDL scancodes; mouse buttons are pseudo-scancodes 600..604. - **Melee killbot**: 360° silent aim for hostile NPCs inside crowbar/melee range, sorted by distance, only while the crowbar is held. -- **Jumpbug**: automate the HL2 jumpbug movement glitch (trigger conditions - to be defined/tested). - **GUI**: replace/augment the VGUI ISurface menu with an immediate-mode GUI (Dear ImGui or a custom implementation); needs a design grill first. - **Distance weapon spread thesis**: TBD — user will explain. Likely models diff --git a/src/hl2h.cpp b/src/hl2h.cpp index 4947b18..125d94e 100644 --- a/src/hl2h.cpp +++ b/src/hl2h.cpp @@ -166,7 +166,184 @@ static void apply_autostrafe(CUserCmd *cmd, const float *vel) { cmd->sidemove = (dx * rx + dy * ry) * 450.0f; } -static void do_move(int seq) { +// --------------------------------------------------------------------------- +// 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 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; +} + +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; + logf("jumpbug: tracking x=%.1f vz=%.1f impact=%.1f", x, vz, impact); + } + + 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 < 20) { + jb_logs++; + logf("jumpbug: trigger x=%.2f drop=%.2f vz=%.1f", x, jb_drop, vz); + } + } else { + // Hold the crouch and wait for the window. Staying ducked is required + // for the unduck trick; releasing early would just add a phase shift + // and fight a user who is holding crouch. + cmd->buttons |= IN_DUCK; + jb_state = 1; + } + } else { + if (jb_state == 1) + jb_state = 0; // expected to be ducked but is not + if (x <= jb_drop + 2.0f) { + // Setup frame: raise the feet 36 units here so the next frame starts + // near the middle of the unduck window. + cmd->buttons |= IN_DUCK; + jb_state = 1; + if (jb_logs < 20) { + jb_logs++; + logf("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; +} + +static void do_move(int seq, float frametime) { static bool g_echo_sent = false; if (!g_input || !g_net.ok()) return; @@ -206,6 +383,7 @@ static void do_move(int seq) { 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); bool bhop_on = feature_active(FEAT_BHOP); bool abh_on = feature_active(FEAT_ABH); @@ -217,7 +395,6 @@ static void do_move(int seq) { 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; @@ -260,6 +437,9 @@ static void do_move(int seq) { } } + if (jumpbug_apply(cmd, localIdx, ent, flags, vel, frametime)) + note_modified(cmd, flags, vel); + if (!feature_active(FEAT_AIMBOT) || !feature_has_bind(FEAT_AIMBOT)) aimbot_reset_lock(); if (!feature_active(FEAT_SILENTAIM) || !feature_has_bind(FEAT_SILENTAIM)) @@ -361,7 +541,7 @@ static void hkdPaintTraverseEx(void *self, long panel, bool paintPopups) { static bool hkdCreateMove(void *self, int seq, float frametime, bool active) { bool ret = oCreateMove(self, seq, frametime, active); - do_move(seq); + do_move(seq, frametime); // Hook ISurface once: PaintTraverseEx (slot 114) fires every frame; keep // DrawPrintText (slot 22) hooked as a fallback text-draw trigger. diff --git a/src/hl2h.h b/src/hl2h.h index fdc88c1..92ae3f1 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -128,8 +128,8 @@ struct CUserCmd { // --------------------------------------------------------------------------- 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 }; +enum { FL_ONGROUND = 1 << 0, FL_DUCKING = 1 << 1, FL_INWATER = 1 << 9 }; +enum { MOVETYPE_WALK = 2, MOVETYPE_NOCLIP = 8, MOVETYPE_LADDER = 9 }; // --------------------------------------------------------------------------- // vector math @@ -152,7 +152,8 @@ enum FeatureId { FEAT_ESP = 2, FEAT_AIMBOT = 3, FEAT_TRIGGERBOT = 4, - FEAT_SILENTAIM = 5 + FEAT_SILENTAIM = 5, + FEAT_JUMPBUG = 6 }; enum EspOption { ESP_OPT_SHOW_NEUTRAL = 0, @@ -200,6 +201,7 @@ bool world_to_screen(const Vector &w, float &sx, float &sy); void set_silent_restore(const QAngle &angles); bool is_visible(const Vector &eye, const Vector &point); void *trace_entity(const Vector &start, const Vector &end, void *ignore); +float ground_distance(const Vector &origin, void *ignore, float max_dist); // bones.cpp — client-side animated skeleton / hitbox access (see there) bool bones_update(int idx, void *ent); diff --git a/src/menu.cpp b/src/menu.cpp index 3e20cb3..0ed89d4 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -62,6 +62,7 @@ static FeatureState g_bhop{true, 0}; static bool g_bhop_autostrafe = true; static FeatureState g_abh{true, 0}; +static FeatureState g_jumpbug{true, 0}; static FeatureState g_esp{true, 0}; static FeatureState g_aimbot{true, 604}; // default Mouse5, rebindable in menu static FeatureState g_triggerbot{true, 603}; // default Mouse4 @@ -96,6 +97,8 @@ static MenuNode g_bhop_active{NODE_TOGGLE, "Active", false, 0, {}, &g_bhop.activ static MenuNode g_bhop_autostrafe_node{NODE_TOGGLE, "Uni-directional auto-strafe", false, 0, {}, &g_bhop_autostrafe, nullptr, nullptr}; static MenuNode g_abh_feat{NODE_FEATURE, "ABH", false, 1, {}, nullptr, nullptr, nullptr}; static MenuNode g_abh_active{NODE_TOGGLE, "Active", false, 0, {}, &g_abh.active, nullptr, nullptr}; +static MenuNode g_jumpbug_feat{NODE_FEATURE, "Jumpbug", false, 1, {}, nullptr, nullptr, nullptr}; +static MenuNode g_jumpbug_active{NODE_TOGGLE, "Active", false, 0, {}, &g_jumpbug.active, nullptr, nullptr}; static MenuNode g_esp_feat{NODE_FEATURE, "ESP", false, 1, {}, nullptr, nullptr, nullptr}; static MenuNode g_esp_active{NODE_TOGGLE, "Active", false, 0, {}, &g_esp.active, nullptr, nullptr}; static MenuNode g_esp_opt_nodes[ESP_OPT_COUNT] = { @@ -145,6 +148,9 @@ static void build_tree() { g_bhop_feat.children[1] = &g_bhop_autostrafe_node; g_bhop_feat.child_count = 2; g_abh_feat.children[0] = &g_abh_active; + g_abh_feat.child_count = 1; + g_jumpbug_feat.children[0] = &g_jumpbug_active; + g_jumpbug_feat.child_count = 1; g_esp_feat.children[0] = &g_esp_active; g_aimbot_feat.children[0] = &g_aimbot_active; g_aimbot_feat.children[1] = &g_aimbot_bind; @@ -169,11 +175,12 @@ static void build_tree() { g_esp_feat.child_count = ESP_OPT_COUNT + 1; g_root.children[0] = &g_bhop_feat; g_root.children[1] = &g_abh_feat; - g_root.children[2] = &g_esp_feat; - g_root.children[3] = &g_aimbot_feat; - g_root.children[4] = &g_triggerbot_feat; - g_root.children[5] = &g_silentaim_feat; - g_root.child_count = 6; + g_root.children[2] = &g_jumpbug_feat; + g_root.children[3] = &g_esp_feat; + g_root.children[4] = &g_aimbot_feat; + g_root.children[5] = &g_triggerbot_feat; + g_root.children[6] = &g_silentaim_feat; + g_root.child_count = 7; } static int g_depth[48]; // depth of each visible row (for indentation) @@ -295,6 +302,7 @@ static void config_save() { fprintf(f, "[bhop]\nactive=%d\nbind=%d\nautostrafe=%d\n", g_bhop.active, g_bhop.bind, g_bhop_autostrafe); fprintf(f, "[abh]\nactive=%d\nbind=%d\n", g_abh.active, g_abh.bind); + fprintf(f, "[jumpbug]\nactive=%d\n", g_jumpbug.active); fprintf(f, "[aimbot]\nactive=%d\nbind=%d\nsmooth=%d\nvischeck=%d\n" "fov=%d\nfov_circle=%d\n" "bone_head=%d\nbone_torso=%d\nbone_larm=%d\nbone_rarm=%d\n" @@ -332,6 +340,8 @@ static void config_load() { cur = &g_bhop; else if (strstr(line, "[abh]")) cur = &g_abh; + else if (strstr(line, "[jumpbug]")) + cur = &g_jumpbug; else if (strstr(line, "[esp]")) cur = &g_esp; else if (strstr(line, "[aimbot]")) @@ -516,10 +526,10 @@ static bool key_pressed(int sc) { // feature gate queried from hl2h.cpp do_move(): feature must be Active AND // its bind key held (always-on when unbound). static FeatureState *feature_state(int feature) { - static FeatureState *feats[6] = {&g_bhop, &g_abh, &g_esp, - &g_aimbot, &g_triggerbot, - &g_silentaim}; - if (feature < 0 || feature > 5) + static FeatureState *feats[7] = {&g_bhop, &g_abh, &g_esp, + &g_aimbot, &g_triggerbot, &g_silentaim, + &g_jumpbug}; + if (feature < 0 || feature > 6) return nullptr; return feats[feature]; } diff --git a/src/vischeck.cpp b/src/vischeck.cpp index 428f54f..6d453c5 100644 --- a/src/vischeck.cpp +++ b/src/vischeck.cpp @@ -95,6 +95,9 @@ constexpr unsigned MASK_VISIBLE = 0x1u | 0x4000u | 0x80u | 0x2000u; // MASK_SHOT = SOLID|MOVEABLE|MONSTER|WINDOW|DEBRIS|HITBOX constexpr unsigned MASK_SHOT = 0x1u | 0x4000u | 0x2000000u | 0x4u | 0x4000000u | 0x40000000u; +// MASK_PLAYERSOLID = SOLID|MOVEABLE|PLAYERCLIP|WINDOW|MONSTER|GRATE +constexpr unsigned MASK_PLAYERSOLID = + 0x1u | 0x4000u | 0x10000u | 0x4u | 0x2000000u | 0x8u; static void make_ray(RayCompat &ray, const Vector &start, const Vector &end) { float dx = end.x - start.x, dy = end.y - start.y, dz = end.z - start.z; @@ -107,6 +110,21 @@ static void make_ray(RayCompat &ray, const Vector &start, const Vector &end) { ray.isSwept = (dx * dx + dy * dy + dz * dz) > 0.0f; } +// Swept AABB matching the standing player hull used by CGameMovement. +// Ray_t::Init shifts m_Start to the box center and stores the negated center +// offset in m_StartOffset. With mins=(-16,-16,0), maxs=(16,16,72), the +// extents are (16,16,36) and m_StartOffset is (0,0,-36). +static void make_standing_hull_ray(RayCompat &ray, const Vector &origin, + float distance) { + ray = RayCompat{}; + ray.start = {origin.x, origin.y, origin.z + 36.0f, 0.0f}; + ray.delta = {0.0f, 0.0f, -distance, 0.0f}; + ray.startOffset = {0.0f, 0.0f, -36.0f, 0.0f}; + ray.extents = {16.0f, 16.0f, 36.0f, 0.0f}; + ray.isRay = false; + ray.isSwept = true; +} + } // namespace bool is_visible(const Vector &eye, const Vector &point) { @@ -147,3 +165,30 @@ void *trace_entity(const Vector &start, const Vector &end, void *ignore) { return nullptr; return tr.ent; } + +// Vertical distance from the standing player hull's feet to the first +// standable surface below. Matches CategorizePosition's 2-unit probe and its +// plane.normal.z >= 0.7 rule, but sweeps much farther so the caller can +// predict the landing. Returns -1 when there is no usable ground. +float ground_distance(const Vector &origin, void *ignore, float max_dist) { + if (!g_iface.enginetrace || max_dist <= 0.0f) + return -1.0f; + RayCompat ray; + make_standing_hull_ray(ray, origin, max_dist); + GameTraceCompat tr{}; + EntityFilter filter(ignore); + using TraceRayFn = void (*)(void *, const RayCompat *, unsigned, void *, + GameTraceCompat *); + vfunc(g_iface.enginetrace, 4)(g_iface.enginetrace, &ray, + MASK_PLAYERSOLID, &filter, &tr); + if (tr.startsolid || tr.fraction >= 1.0f || tr.normal.z < 0.7f) { + static int solid_logs = 0; + if (tr.startsolid && solid_logs < 4) { + solid_logs++; + logf("ground trace: startsolid at (%.1f %.1f %.1f)", origin.x, + origin.y, origin.z); + } + return -1.0f; + } + return tr.fraction * max_dist; +} -- cgit v1.2.3