From 471f022f449a18766fedb1ff0a4204a933b95b9c Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 27 Oct 2026 13:48:27 +0100 Subject: Jumpbug: optional velocity-timed assist The unduck window is only 2 units, while a 625-ups fall moves ~9.3 units per 66.7 Hz movement command, so pure jumpbug is ~20% reliable at that speed. - Add Jumpbug -> Assist timing (default off). When the natural next command would skip [36,38], pin the post-command ducked distance to 37 by writing the vertical velocity for that command. Pure jumpbug remains available with the option off. - Resolve the client local m_vecAbsVelocity (0x11C) via the client datamap and the server m_vecAbsVelocity (0x228) via the server datamap. - Write both the authoritative server velocity (what SetupMove reads through GetAbsVelocity) and the local prediction fields, so the next CreateMove sees the assisted origin on both sides. - Do not write the server m_vecVelocity directly: the datamap scan finds two same-named entries (0x1C and 0x29C); SetAbsVelocity syncs it after movement. --- CONTEXT.md | 17 ++++++++++++ src/hl2h.cpp | 82 +++++++++++++++++++++++++++++++++++++++++++++++++-------- src/hl2h.h | 3 +++ src/menu.cpp | 17 +++++++++--- src/netvars.cpp | 67 ++++++++++++++++++++++++++++++++++++++++++++-- src/server.cpp | 23 ++++++++++++++-- 6 files changed, 191 insertions(+), 18 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index b1ec4e7..38d9373 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -80,6 +80,7 @@ Relevant vtable slots (this build): | `m_vecMins` / `m_vecMaxs` | `0x1A8` / `0x1B4` | | `m_lifeState` | `0x7F` | | `m_nModelIndex` | `0x7C` | +| `m_vecAbsVelocity` (client local, not netvar) | `0x11C` | - Eye position = `m_vecOrigin + m_vecViewOffset` on **all three axes**. (The old code added the X component to Z, which left the eye ~64 units low.) @@ -106,6 +107,7 @@ HL2hake Active Jumpbug Active + Assist timing ESP Active Show neutral / healthbar / healthnum / name / skeleton / box / speed graph @@ -181,6 +183,21 @@ Binds use SDL scancodes; mouse buttons are pseudo-scancodes 600..604. - `drop` is measured from consecutive predicted origins (not a hard-coded tick interval). At high fall speeds the 2-unit band can be skipped between commands; a higher frame rate improves reliability. Best-effort by design. +- **Assist timing** (off by default): when the natural next command would jump + over the 2-unit window, force the post-command distance to 37 by writing the + vertical velocity. It needs both the authoritative server velocity and the + client prediction velocity: + - server `CBaseEntity::m_vecAbsVelocity` (datamap offset 0x228), read by + `SetupMove` via `GetAbsVelocity`; + - client `C_BaseEntity::m_vecAbsVelocity` (datamap offset 0x11C) and the + networked `m_vecVelocity` (0xE0 + 8). + The velocity is `dz / 0.015 + 0.5 * 600 * 0.015` where `dz` is the required + displacement for this command. This turns the ~20% timing success at 625 ups + into a repeatable setup, at the cost of one command with a nudged vertical + speed. Pure jumpbug remains available with Assist off. +- The server datamap has two `m_vecVelocity` entries (0x1C and 0x29C), so the + assist deliberately writes only `m_vecAbsVelocity`; `SetAbsVelocity` syncs + `m_vecVelocity` after the command. ## Gotchas / lessons diff --git a/src/hl2h.cpp b/src/hl2h.cpp index 125d94e..4ba30bc 100644 --- a/src/hl2h.cpp +++ b/src/hl2h.cpp @@ -186,6 +186,8 @@ static void apply_autostrafe(CUserCmd *cmd, const float *vel) { 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; @@ -213,6 +215,30 @@ static void jumpbug_reset() { 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 || @@ -285,6 +311,10 @@ static bool jumpbug_apply(CUserCmd *cmd, int local, void *ent, int flags, logf("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 @@ -310,29 +340,59 @@ static bool jumpbug_apply(CUserCmd *cmd, int local, void *ent, int flags, trigger = true; jb_state = 0; jb_tracked = false; - if (jb_logs < 20) { + if (jb_logs < 30) { jb_logs++; - logf("jumpbug: trigger x=%.2f drop=%.2f vz=%.1f", x, jb_drop, vz); + logf("jumpbug: trigger x=%.2f drop=%.2f vz=%.1f assist=%d", x, jb_drop, + vz, (int)assist); } } 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. + 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++; + logf("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 - 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. + 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 < 20) { + if (jb_logs < 30) { jb_logs++; - logf("jumpbug: setup x=%.2f drop=%.2f pred=%.2f vz=%.1f", x, jb_drop, - x + JB_DUCK_RAISE - jb_drop, vz); + if (assist) + logf("jumpbug: setup assist x=%.2f drop=%.2f vz=%.1f ok=%d", x, + jb_drop, vel[2], (int)forced); + else + logf("jumpbug: setup x=%.2f drop=%.2f pred=%.2f vz=%.1f", x, + jb_drop, x + JB_DUCK_RAISE - jb_drop, vz); } } } diff --git a/src/hl2h.h b/src/hl2h.h index 92ae3f1..08eb2ef 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -81,6 +81,7 @@ struct NetVars { int maxs = -1; int lifestate = -1; int modelindex = -1; + int absvelocity = -1; // client C_BaseEntity::m_vecAbsVelocity (prediction) // ponytail: mins/maxs may be absent from some HL2 netvar tables; esp.cpp falls back to h=72 bool ok() const { return flags >= 0 && velocity >= 0 && movetype >= 0; } // ponytail: m_iMaxHealth isn't a netvar in HL2 SP; esp.cpp falls back to 100. @@ -177,6 +178,7 @@ bool find_input_global(); void resolve_netvars(); bool resolve_server_netvars(); int server_entity_health(int idx, int *maxhealth, int *lifestate, void **base_out); +bool server_set_velocity_z(int idx, float vz); int server_model_index(void *base); void find_modules(); void init_conmsg(); @@ -185,6 +187,7 @@ bool feature_has_bind(int feature); bool feature_bind_held(int feature); bool esp_option(int opt); bool bhop_autostrafe_enabled(); +bool jumpbug_assist_enabled(); unsigned aimbot_bone_mask(); int aimbot_smooth_step(); bool aimbot_vischeck_enabled(); diff --git a/src/menu.cpp b/src/menu.cpp index 0ed89d4..4f76529 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -63,6 +63,7 @@ static bool g_bhop_autostrafe = true; static FeatureState g_abh{true, 0}; static FeatureState g_jumpbug{true, 0}; +static bool g_jumpbug_assist = false; 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 @@ -99,6 +100,7 @@ static MenuNode g_abh_feat{NODE_FEATURE, "ABH", false, 1, {}, 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_jumpbug_assist_node{NODE_TOGGLE, "Assist timing", false, 0, {}, &g_jumpbug_assist, 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] = { @@ -150,7 +152,8 @@ static void build_tree() { 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_jumpbug_feat.children[1] = &g_jumpbug_assist_node; + g_jumpbug_feat.child_count = 2; g_esp_feat.children[0] = &g_esp_active; g_aimbot_feat.children[0] = &g_aimbot_active; g_aimbot_feat.children[1] = &g_aimbot_bind; @@ -302,7 +305,8 @@ 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, "[jumpbug]\nactive=%d\nassist=%d\n", g_jumpbug.active, + (int)g_jumpbug_assist); 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,7 +336,8 @@ static void config_load() { return; char line[256]; FeatureState *cur = nullptr; - bool in_esp = false, in_bhop = false, in_aim = false, in_silent = false; + bool in_esp = false, in_bhop = false, in_aim = false, in_silent = false, + in_jumpbug = false; while (fgets(line, sizeof(line), f)) { line[strcspn(line, "\r\n")] = 0; if (line[0] == '[') { @@ -356,6 +361,7 @@ static void config_load() { in_bhop = (cur == &g_bhop); in_aim = (cur == &g_aimbot); in_silent = (cur == &g_silentaim); + in_jumpbug = (cur == &g_jumpbug); } else if (cur) { char *eq = strchr(line, '='); if (!eq) @@ -365,6 +371,9 @@ static void config_load() { if (in_bhop && strcmp(key, "autostrafe") == 0) { g_bhop_autostrafe = atoi(val) != 0; } + if (in_jumpbug && strcmp(key, "assist") == 0) { + g_jumpbug_assist = atoi(val) != 0; + } if (in_aim) { if (strcmp(key, "smooth") == 0) { int v = atoi(val); @@ -557,6 +566,8 @@ bool esp_option(int opt) { bool bhop_autostrafe_enabled() { return g_bhop_autostrafe; } +bool jumpbug_assist_enabled() { return g_jumpbug_assist; } + unsigned aimbot_bone_mask() { unsigned m = 0; if (g_aim_bones[AIM_BONE_HEAD]) diff --git a/src/netvars.cpp b/src/netvars.cpp index b4e31d5..98ebe31 100644 --- a/src/netvars.cpp +++ b/src/netvars.cpp @@ -185,6 +185,66 @@ static bool resolve_movetype() { return true; } +static bool resolve_absvelocity() { + static const char needle[] = "m_vecAbsVelocity"; + 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_vecAbsVelocity 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 != 3 || 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_vecAbsVelocity typedescription not found"); + return false; + } + int best = 0; + for (int k = 1; k < ncand; k++) + if (counts[k] > counts[best]) + best = k; + g_net.absvelocity = (int)offs[best]; + return true; +} + void resolve_netvars() { using GetAllClassesFn = void *(*)(void *); uintptr_t head = @@ -205,10 +265,13 @@ void resolve_netvars() { // movetype unresolved — bhop still works, just no noclip/ladder guard logf("movetype guard disabled"); } + if (g_net.absvelocity < 0 && !resolve_absvelocity()) + logf("m_vecAbsVelocity unresolved; jumpbug assist disabled"); logf("netvars: flags=%d velocity=%d movetype=%d health=%d maxhealth=%d " "teamnum=%d origin=%d viewoffset=%d mins=%d maxs=%d lifeste=%d " - "modelindex=%d", + "modelindex=%d absvelocity=%d", g_net.flags, g_net.velocity, g_net.movetype, g_net.health, g_net.maxhealth, g_net.teamnum, g_net.origin, g_net.viewoffset, - g_net.mins, g_net.maxs, g_net.lifestate, g_net.modelindex); + g_net.mins, g_net.maxs, g_net.lifestate, g_net.modelindex, + g_net.absvelocity); } diff --git a/src/server.cpp b/src/server.cpp index 6feb55b..3766906 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -31,6 +31,8 @@ int g_srv_health = -1; int g_srv_maxhealth = -1; int g_srv_lifestate = -1; int g_srv_modelindex = -1; +int g_srv_absvelocity = -1; +int g_srv_velocity = -1; static bool g_srv_ready = false; static bool g_srv_checked = false; @@ -105,12 +107,14 @@ bool resolve_server_netvars() { g_srv_maxhealth = datamap_offset("m_iMaxHealth", FIELD_INTEGER); g_srv_lifestate = datamap_offset("m_lifeState", FIELD_CHARACTER); g_srv_modelindex = datamap_offset("m_nModelIndex", 7); // FIELD_SHORT + g_srv_absvelocity = datamap_offset("m_vecAbsVelocity", 3); // FIELD_VECTOR + g_srv_velocity = datamap_offset("m_vecVelocity", 3); // FIELD_VECTOR g_srv_ready = g_srv_health > 0; if (g_srv_ready) { logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " - "modelindex=%d", + "modelindex=%d absvel=%d vel=%d", g_server.path.c_str(), g_srv_health, g_srv_maxhealth, g_srv_lifestate, - g_srv_modelindex); + g_srv_modelindex, g_srv_absvelocity, g_srv_velocity); fail_logged = false; } else if (!fail_logged) { fail_logged = true; @@ -175,3 +179,18 @@ int server_model_index(void *base) { return -1; return (int)*(short *)((uintptr_t)base + g_srv_modelindex); } + +// Set the authoritative vertical velocity of a server entity. m_vecAbsVelocity +// is what CGameMovement::SetupMove reads through GetAbsVelocity(); writing the +// networked m_vecVelocity directly is deliberately avoided because the server +// datamap scan can find a same-named field on another class first. The movement +// code syncs m_vecVelocity through SetAbsVelocity after the command. +bool server_set_velocity_z(int idx, float vz) { + if (!g_srv_ready || !g_iface.engineserver || g_srv_absvelocity <= 0) + return false; + void *base = nullptr; + if (server_entity_health(idx, nullptr, nullptr, &base) < 0 || !base) + return false; + *(float *)((uintptr_t)base + g_srv_absvelocity + 8) = vz; + return true; +} -- cgit v1.2.3