From ba500fef7fbf94725f18541b0e23a45591379dba Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 27 Oct 2026 20:15:51 +0100 Subject: Fix tooltip truncation; add Stucklaunch assist Tooltips: text_in() shifted the x position to fit the full string before truncating, which drew the tooltip text outside its box. Truncate to the available width first, then clamp inside the clip. Stucklaunch (Movement): - opt-in feature (Active off by default) with its own bind; - holds jump, alternates +use every 2 ticks to regrab a physics prop, optionally +attack while the gravity gun is active, and air-strafes; - engagement uses m_hGroundEntity (client netvar) latched for ~0.5 s so the prop stays active while airborne; falls back to running whenever active if the netvar is unavailable, with a startup warning to bind it; - does not spawn or place props; the player supplies the prop and initial speed. Config gets [stucklaunch] active/bind/use_spam/gravspam/autostrafe; Reset restores those defaults. CONTEXT documents the assist and its limits. --- CONTEXT.md | 20 ++++++++++++++++---- src/esp.cpp | 34 ++++++++++++++++++++++++++++++++++ src/gui.cpp | 47 +++++++++++++++++++++++++++++++++++++---------- src/hl2h.cpp | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++--- src/hl2h.h | 15 +++++++++++++-- src/menu.cpp | 40 +++++++++++++++++++++++++++++++++++----- src/netvars.cpp | 5 ++++- 7 files changed, 190 insertions(+), 25 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 78f7c75..d7ea752 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -264,12 +264,24 @@ vocabulary / decisions. - Aimbot vischeck (`MASK_VISIBLE`/world-only) differs from triggerbot (`MASK_SHOT`/all entities): glass and static props can behave differently. +## Stucklaunch assist + +- Optional Movement feature; Active is off by default. +- While engaged it holds jump, alternates +use every 2 ticks to regrab a + physics prop, optionally alternates +attack while the gravity gun is + active, and air-strafes the hops. +- Engagement comes from `m_hGroundEntity`, latched for ~0.5 s so the prop + stays "active" during each hop. If that netvar is unavailable it runs + whenever the feature is active, so bind it to a key. +- It does not spawn or move props. The player still supplies the prop and the + small initial speed. Pallets and brush physboxes work best. + ## Roadmap / TODO -- **GUI**: replace/augment the VGUI ISurface menu with an immediate-mode GUI - (Dear ImGui or a custom implementation); needs a design grill first. -- **Server-side spread/recoil control**: optional single-player writes to the - pistol accuracy penalty and view punch, and a true zero-cone hook if wanted. +- **Stucklaunch tuning**: use-spam period, prop class coverage, and a possible + pause/unpause variant still need in-game testing. +- **Server-side spread/recoil control**: not planned; recoil and accuracy + penalties are intentionally left stock. - **Distance weapon spread thesis**: addressed by Spread control; stock HL2 hitscan damage is range-independent, so distance efficiency is the cone. diff --git a/src/esp.cpp b/src/esp.cpp index 64ee83f..bda6232 100644 --- a/src/esp.cpp +++ b/src/esp.cpp @@ -1131,6 +1131,40 @@ static void spread_compute() { g_spread_ready = true; } +bool entity_is_physics_prop(int idx) { + if (idx <= 0 || idx > 4096) + return false; + void *net = entity_networkable(idx); + const char *cls = net ? entity_class_name(net, nullptr) : nullptr; + void *ent = entitylist_get(idx); + const char *model = ent ? entity_model_name(ent) : nullptr; + if (cls && (strstr(cls, "prop_physics") || strstr(cls, "physbox"))) + return true; + if (model && (strstr(model, "props_junk") || strstr(model, "props_physics"))) + return true; + return false; +} + +bool active_weapon_is_gravity_gun(void *local_ent) { + if (!local_ent || g_net.activeweapon < 0) + return false; + uint32_t h = *(uint32_t *)((uintptr_t)local_ent + g_net.activeweapon); + if (h == 0xFFFFFFFFu) + return false; + int idx = (int)(h & 0xFFFF); + if (idx <= 0 || idx > 4096) + return false; + void *wep = entitylist_get(idx); + void *net = entity_networkable(idx); + const char *cls = net ? entity_class_name(net, nullptr) : nullptr; + const char *model = wep ? entity_model_name(wep) : nullptr; + if (cls && strstr(cls, "physcannon")) + return true; + if (model && (strstr(model, "physcannon") || strstr(model, "w_physics"))) + return true; + return false; +} + static bool active_weapon_is_shotgun(void *local_ent) { if (!local_ent || g_net.activeweapon < 0) return false; diff --git a/src/gui.cpp b/src/gui.cpp index 0d5b458..dc0e4eb 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -293,6 +293,19 @@ static void build_cards() { add_card(0, &g_abh, "ABH", "Accelerate on the jump tick by facing away " "from the velocity.", true); + int sl = add_card(0, &g_stucklaunch, "Stucklaunch", + "Hop on a physics prop while spamming grab to build " + "speed in a small space.", + true); + add_toggle(sl, "Use spam", + "Rapidly press +use to keep regrabbing the prop.", + &g_stucklaunch_use_spam); + add_toggle(sl, "Gravity gun spam", + "Rapidly press +attack while holding the gravity gun.", + &g_stucklaunch_gravspam); + add_toggle(sl, "Auto-strafe", "Air-strafe each hop.", + &g_stucklaunch_autostrafe); + int jb = add_card(0, &g_jumpbug, "Jumpbug", "Duck+jump when landing would hurt.", true); add_toggle(jb, "Assist timing", @@ -523,26 +536,40 @@ static void text_in(const Rect &clip, int x, int y, const char *s, return; if (x >= clip.x + clip.w) return; - if (x + text_w(s, tall) > clip.x + clip.w) - x = clip.x + clip.w - text_w(s, tall); - if (max_w > 0 && text_w(s, tall) > max_w) { - char buf[256]; + + // Available space from x to the clip edge, optionally capped by max_w. + // Truncate first, then clamp: shifting before truncating used to draw the + // text outside the tooltip/button box. + int avail = clip.x + clip.w - x; + if (max_w > 0 && max_w < avail) + avail = max_w; + if (avail <= 0) + return; + + char buf[256]; + const char *draw = s; + int w = text_w(s, tall); + if (w > avail) { snprintf(buf, sizeof(buf), "%s", s); int n = (int)strlen(buf); - while (n > 3 && text_w(buf, tall) > max_w) + int dots = text_w("...", tall); + while (n > 0 && text_w(buf, tall) + dots > avail) buf[--n] = 0; - if (n > 3 && n < (int)sizeof(buf) - 3) { + if (n > 0 && n + 3 < (int)sizeof(buf)) { buf[n++] = '.'; buf[n++] = '.'; buf[n++] = '.'; buf[n] = 0; } - s_text_color(c.r, c.g, c.b, c.a); - draw_text_raw(x, y, buf, font, tall); - return; + draw = buf; + w = text_w(draw, tall); } + if (x + w > clip.x + clip.w) + x = clip.x + clip.w - w; + if (x < clip.x) + x = clip.x; s_text_color(c.r, c.g, c.b, c.a); - draw_text_raw(x, y, s, font, tall); + draw_text_raw(x, y, draw, font, tall); } static Rect clip_rect(const Rect &r, const Rect &clip) { diff --git a/src/hl2h.cpp b/src/hl2h.cpp index c51d35e..63fdcdc 100644 --- a/src/hl2h.cpp +++ b/src/hl2h.cpp @@ -169,6 +169,44 @@ static void apply_autostrafe(CUserCmd *cmd, const float *vel) { cmd->sidemove = (dx * rx + dy * ry) * 450.0f; } +// True while the player stands on (or just left) a physics prop. m_hGroundEntity +// is latched briefly because it clears during each hop. +static bool stucklaunch_prop_ok(void *ent, int tick) { + if (g_net.groundentity < 0) + return true; // no netvar: the bind is the only safeguard + static int last_prop_tick = -100000; + uint32_t h = *(uint32_t *)((uintptr_t)ent + g_net.groundentity); + int gi = (h == 0xFFFFFFFFu) ? 0 : (int)(h & 0xFFFF); + bool on = gi > 0 && gi <= 4096 && entity_is_physics_prop(gi); + if (on) + last_prop_tick = tick; + return tick - last_prop_tick < 32; // ~0.5 s at 66 tick +} + +// Stucklaunch assist. The player supplies the physics prop and a little +// initial speed; this holds jump, alternates +use (and optionally the gravity +// gun's +attack) to keep regrabbing the prop, and air-strafes the hops. +static void stucklaunch_apply(CUserCmd *cmd, void *local_ent, bool onground, + const float *vel) { + cmd->buttons |= IN_JUMP; + int phase = (cmd->tick_count / 2) & 1; + if (stucklaunch_use_spam_enabled()) { + if (phase == 0) + cmd->buttons |= IN_USE; + else + cmd->buttons &= ~IN_USE; + } + if (stucklaunch_gravspam_enabled() && + active_weapon_is_gravity_gun(local_ent)) { + if (phase == 0) + cmd->buttons |= IN_ATTACK; + else + cmd->buttons &= ~IN_ATTACK; + } + if (stucklaunch_autostrafe_enabled() && !onground) + apply_autostrafe(cmd, vel); +} + // --------------------------------------------------------------------------- // jumpbug // --------------------------------------------------------------------------- @@ -576,8 +614,15 @@ static void do_move(int seq, float frametime) { return; } - bool bhop_on = feature_active(FEAT_BHOP); - bool abh_on = feature_active(FEAT_ABH); + bool stuck_on = feature_active(FEAT_STUCKLAUNCH); + bool stuck_engaged = stuck_on && stucklaunch_prop_ok(ent, cmd->tick_count); + if (stuck_engaged) { + stucklaunch_apply(cmd, ent, onground, vel); + note_modified(cmd, flags, vel); + } + + bool bhop_on = !stuck_engaged && feature_active(FEAT_BHOP); + bool abh_on = !stuck_engaged && feature_active(FEAT_ABH); static bool lastground = false; @@ -628,7 +673,8 @@ static void do_move(int seq, float frametime) { } } - if (jumpbug_apply(cmd, localIdx, ent, flags, vel, frametime)) + if (!stuck_engaged && + jumpbug_apply(cmd, localIdx, ent, flags, vel, frametime)) note_modified(cmd, flags, vel); bool melee = feature_active(FEAT_MELEE) && melee_weapon_active(ent); @@ -866,6 +912,8 @@ static void *setup_thread(void *) { 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_net.groundentity < 0) + toastf(TOAST_WARN, "Stucklaunch prop detection unavailable; bind it"); if (!g_iface.engineserver) toastf(TOAST_WARN, "Server interfaces missing; jumpbug/spread limited"); if (!g_iface.debugoverlay) diff --git a/src/hl2h.h b/src/hl2h.h index e390aa0..aabe8e9 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -92,6 +92,7 @@ struct NetVars { int modelindex = -1; int absvelocity = -1; // client C_BaseEntity::m_vecAbsVelocity (prediction) int activeweapon = -1; // client m_hActiveWeapon handle + int groundentity = -1; // client m_hGroundEntity handle // 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. @@ -171,7 +172,9 @@ enum FeatureId { FEAT_SILENTAIM = 5, FEAT_JUMPBUG = 6, FEAT_MELEE = 7, - FEAT_SPREAD = 8 + FEAT_SPREAD = 8, + FEAT_STUCKLAUNCH = 9, + FEAT_COUNT = 10 }; enum EspOption { ESP_OPT_SHOW_NEUTRAL = 0, @@ -203,10 +206,13 @@ struct FeatureState { }; extern FeatureState g_bhop, g_abh, g_jumpbug, g_melee, g_spread, g_esp, - g_aimbot, g_triggerbot, g_silentaim; + g_aimbot, g_triggerbot, g_silentaim, g_stucklaunch; extern bool g_bhop_autostrafe; extern bool g_jumpbug_assist; extern bool g_spread_no_spread; +extern bool g_stucklaunch_use_spam; +extern bool g_stucklaunch_gravspam; +extern bool g_stucklaunch_autostrafe; extern bool g_aim_bones[AIM_BONE_COUNT]; extern int g_aim_smooth; extern bool g_aim_vischeck; @@ -245,6 +251,11 @@ bool esp_option(int opt); bool bhop_autostrafe_enabled(); bool jumpbug_assist_enabled(); bool spread_no_spread_enabled(); +bool stucklaunch_use_spam_enabled(); +bool stucklaunch_gravspam_enabled(); +bool stucklaunch_autostrafe_enabled(); +bool active_weapon_is_gravity_gun(void *local_ent); +bool entity_is_physics_prop(int idx); unsigned aimbot_bone_mask(); int aimbot_smooth_step(); bool aimbot_vischeck_enabled(); diff --git a/src/menu.cpp b/src/menu.cpp index 3d008bb..00ba9be 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -44,6 +44,10 @@ FeatureState g_esp{true, 0}; FeatureState g_aimbot{true, 604}; // default Mouse5, rebindable in the GUI FeatureState g_triggerbot{true, 603}; // default Mouse4 FeatureState g_silentaim{true, 602}; // default Mouse3 +FeatureState g_stucklaunch{false, 0}; // opt-in movement assist +bool g_stucklaunch_use_spam = true; +bool g_stucklaunch_gravspam = false; +bool g_stucklaunch_autostrafe = true; // Aimbot bone checklist. Head defaults on (v1 behaviour). bool g_aim_bones[AIM_BONE_COUNT] = {true, false, false, @@ -158,6 +162,11 @@ 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, "[stucklaunch]\nactive=%d\nbind=%d\nuse_spam=%d\n" + "gravspam=%d\nautostrafe=%d\n", + g_stucklaunch.active, g_stucklaunch.bind, + (int)g_stucklaunch_use_spam, (int)g_stucklaunch_gravspam, + (int)g_stucklaunch_autostrafe); fprintf(f, "[jumpbug]\nactive=%d\nbind=%d\nassist=%d\n", g_jumpbug.active, g_jumpbug.bind, (int)g_jumpbug_assist); fprintf(f, "[melee]\nactive=%d\nbind=%d\n", g_melee.active, g_melee.bind); @@ -204,7 +213,7 @@ static bool config_load() { char line[256]; FeatureState *cur = nullptr; bool in_esp = false, in_bhop = false, in_aim = false, in_silent = false, - in_jumpbug = false, in_spread = false; + in_jumpbug = false, in_spread = false, in_stucklaunch = false; while (fgets(line, sizeof(line), f)) { line[strcspn(line, "\r\n")] = 0; if (line[0] == '[') { @@ -212,6 +221,8 @@ static bool config_load() { cur = &g_bhop; else if (strstr(line, "[abh]")) cur = &g_abh; + else if (strstr(line, "[stucklaunch]")) + cur = &g_stucklaunch; else if (strstr(line, "[jumpbug]")) cur = &g_jumpbug; else if (strstr(line, "[melee]")) @@ -234,6 +245,7 @@ static bool config_load() { in_silent = (cur == &g_silentaim); in_jumpbug = (cur == &g_jumpbug); in_spread = (cur == &g_spread); + in_stucklaunch = (cur == &g_stucklaunch); } else if (cur) { char *eq = strchr(line, '='); if (!eq) @@ -249,6 +261,14 @@ static bool config_load() { if (in_spread && strcmp(key, "no_spread") == 0) { g_spread_no_spread = atoi(val) != 0; } + if (in_stucklaunch) { + if (strcmp(key, "use_spam") == 0) + g_stucklaunch_use_spam = atoi(val) != 0; + else if (strcmp(key, "gravspam") == 0) + g_stucklaunch_gravspam = atoi(val) != 0; + else if (strcmp(key, "autostrafe") == 0) + g_stucklaunch_autostrafe = atoi(val) != 0; + } if (in_aim) { if (strcmp(key, "smooth") == 0) { int v = atoi(val); @@ -327,6 +347,10 @@ void config_reset_defaults() { g_bhop = {true, 0}; g_bhop_autostrafe = true; g_abh = {true, 0}; + g_stucklaunch = {false, 0}; + g_stucklaunch_use_spam = true; + g_stucklaunch_gravspam = false; + g_stucklaunch_autostrafe = true; g_jumpbug = {true, 0}; g_jumpbug_assist = false; g_melee = {true, 0}; @@ -432,10 +456,10 @@ static bool key_held(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[9] = {&g_bhop, &g_abh, &g_esp, - &g_aimbot, &g_triggerbot, &g_silentaim, - &g_jumpbug, &g_melee, &g_spread}; - if (feature < 0 || feature > 8) + static FeatureState *feats[FEAT_COUNT] = { + &g_bhop, &g_abh, &g_esp, &g_aimbot, &g_triggerbot, + &g_silentaim, &g_jumpbug, &g_melee, &g_spread, &g_stucklaunch}; + if (feature < 0 || feature >= FEAT_COUNT) return nullptr; return feats[feature]; } @@ -467,6 +491,12 @@ bool jumpbug_assist_enabled() { return g_jumpbug_assist; } bool spread_no_spread_enabled() { return g_spread_no_spread; } +bool stucklaunch_use_spam_enabled() { return g_stucklaunch_use_spam; } + +bool stucklaunch_gravspam_enabled() { return g_stucklaunch_gravspam; } + +bool stucklaunch_autostrafe_enabled() { return g_stucklaunch_autostrafe; } + 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 3b2d3b8..e304317 100644 --- a/src/netvars.cpp +++ b/src/netvars.cpp @@ -92,6 +92,9 @@ static void collect_netvars(uintptr_t table, uintptr_t base_off, int depth) { } else if (strcmp(pname2, "m_hActiveWeapon") == 0 && g_net.activeweapon < 0) { g_net.activeweapon = (int)(base_off + (uint32_t)poff); + } else if (strcmp(pname2, "m_hGroundEntity") == 0 && + g_net.groundentity < 0) { + g_net.groundentity = (int)(base_off + (uint32_t)poff); } else if (strcmp(pname2, "m_iTeamNum") == 0 && g_net.teamnum < 0) { g_net.teamnum = (int)(base_off + (uint32_t)poff); } else if (strcmp(pname2, "m_vecOrigin") == 0 && g_net.origin < 0) { @@ -272,7 +275,7 @@ void resolve_netvars() { 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 absvelocity=%d activeweapon=%d", + "modelindex=%d absvelocity=%d activeweapon=%d groundentity=%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, -- cgit v1.2.3