From 27c9ad766e1b1333922b4c14cbde9d9b77c8dab0 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 27 Oct 2026 15:22:19 +0100 Subject: Spread control: client-seed no-spread HL2's CBaseEntity::FireBullets seeds its RNG from the usercmd random_seed (m_bUseServerRandomSeed is false for HL2 weapons), so the client can select the 8-bit seed that puts CShotManipulator::ApplySpread closest to the center. - New feature Spread control -> Active / No spread (both default on). - Precompute the best seed for a single shot and for the shotgun's 8-shot primary (first pellet accurate) and 12-shot secondary windows using the engine's own RandomSeed/RandomFloat from libvstdlib. - Set cmd->random_seed on attack commands; note_modified repairs the CRC. - The random cone offset is neutralized, but the cone size, the pistol accuracy penalty, server view punch and any damage falloff remain server-side; stock HL2 has no distance damage falloff. --- src/esp.cpp | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/hl2h.cpp | 3 ++ src/hl2h.h | 7 ++- src/menu.cpp | 41 +++++++++++++----- 4 files changed, 174 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/esp.cpp b/src/esp.cpp index 47df57d..0117d27 100644 --- a/src/esp.cpp +++ b/src/esp.cpp @@ -1042,6 +1042,142 @@ bool melee_aimbot_apply(CUserCmd *cmd) { return false; } +// --------------------------------------------------------------------------- +// weapon spread control +// --------------------------------------------------------------------------- +// +// HL2's CBaseEntity::FireBullets uses CBaseEntity::GetPredictionRandomSeed() +// (the client usercmd's random_seed) unless a weapon opts into +// m_bUseServerRandomSeed. HL2 weapons do not, so the client can choose the +// 8-bit seed and with it the random cone offset. CShotManipulator::ApplySpread +// draws four uniform floats per attempt; we precompute the best seed for one +// shot and for the shotgun's pellet windows with the engine's own RNG. + +static bool g_spread_ready = false; +static int g_spread_seed1 = 0; +static int g_spread_seed8 = 0; // first pellet exact, 7 spread +static int g_spread_seed12 = 0; // all 12 spread + +static void spread_compute() { + using SeedFn = void (*)(int); + using FloatFn = float (*)(float, float); + SeedFn seed = (SeedFn)dlsym(RTLD_DEFAULT, "RandomSeed"); + FloatFn rf = (FloatFn)dlsym(RTLD_DEFAULT, "RandomFloat"); + if (!seed || !rf) { + 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) { + seed = (SeedFn)dlsym(h, "RandomSeed"); + rf = (FloatFn)dlsym(h, "RandomFloat"); + } + } + } + if (!seed || !rf) { + logf("spread: RandomSeed/RandomFloat unavailable"); + return; + } + + auto draw = [&](int s, float &x, float &y) { + seed(s); + int it = 0; + do { + // CShotManipulator::ApplySpread with the default bias of 1.0: + // shotBias = 1.0, flatness = 0.5. + x = rf(-1.0f, 1.0f) * 0.5f + rf(-1.0f, 1.0f) * 0.5f; + y = rf(-1.0f, 1.0f) * 0.5f + rf(-1.0f, 1.0f) * 0.5f; + it++; + } while (x * x + y * y > 1.0f && it < 64); + }; + + float best1 = 1e9f; + for (int s = 0; s < 256; s++) { + float x, y; + draw(s, x, y); + float r2 = x * x + y * y; + if (r2 < best1) { + best1 = r2; + g_spread_seed1 = s; + } + } + + auto window = [&](int n, int first) { + int best_s = 0; + float best = 1e9f; + for (int s = 0; s < 256; s++) { + float score = 0.0f; + for (int i = first; i < n; i++) { + float x, y; + draw(s + i, x, y); + score += x * x + y * y; + } + if (score < best) { + best = score; + best_s = s; + } + } + return best_s; + }; + g_spread_seed8 = window(8, 1); + g_spread_seed12 = window(12, 0); + seed(0); + + logf("spread: seeds single=%d shotgun8=%d shotgun12=%d", g_spread_seed1, + g_spread_seed8, g_spread_seed12); + g_spread_ready = true; +} + +static bool active_weapon_is_shotgun(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; + return (cls && strstr(cls, "Shotgun")) || + (model && strstr(model, "shotgun")); +} + +bool spread_control_apply(CUserCmd *cmd, void *local_ent) { + if (!feature_active(FEAT_SPREAD) || !spread_no_spread_enabled()) + return false; + if (!(cmd->buttons & (IN_ATTACK | IN_ATTACK2))) + return false; + if (!g_spread_ready) { + static bool tried = false; + if (!tried) { + tried = true; + spread_compute(); + } + } + if (!g_spread_ready) + return false; + + bool shotgun = active_weapon_is_shotgun(local_ent); + int seed = g_spread_seed1; + if (shotgun) + seed = (cmd->buttons & IN_ATTACK2) ? g_spread_seed12 : g_spread_seed8; + if (cmd->random_seed == seed) + return false; + cmd->random_seed = seed; + + static int logs = 0; + if (logs < 10) { + logs++; + logf("spread: seed=%d (%s)", seed, shotgun ? "shotgun" : "single"); + } + return true; +} + bool triggerbot_apply(CUserCmd *cmd) { if (!g_iface.enginetrace || !g_iface.entitylist) return false; diff --git a/src/hl2h.cpp b/src/hl2h.cpp index ee2e990..dddcbdc 100644 --- a/src/hl2h.cpp +++ b/src/hl2h.cpp @@ -532,6 +532,9 @@ static void do_move(int seq, float frametime) { note_modified(cmd, flags, vel); } } + + if (spread_control_apply(cmd, ent)) + note_modified(cmd, flags, vel); } // --------------------------------------------------------------------------- diff --git a/src/hl2h.h b/src/hl2h.h index 2680591..ef8e8d6 100644 --- a/src/hl2h.h +++ b/src/hl2h.h @@ -129,7 +129,7 @@ struct CUserCmd { // engine constants // --------------------------------------------------------------------------- -enum { IN_ATTACK = 1 << 0, IN_JUMP = 1 << 1, IN_DUCK = 1 << 2, IN_USE = 1 << 5 }; +enum { IN_ATTACK = 1 << 0, IN_JUMP = 1 << 1, IN_DUCK = 1 << 2, IN_USE = 1 << 5, IN_ATTACK2 = 1 << 11 }; enum { FL_ONGROUND = 1 << 0, FL_DUCKING = 1 << 1, FL_INWATER = 1 << 9 }; enum { MOVETYPE_WALK = 2, MOVETYPE_NOCLIP = 8, MOVETYPE_LADDER = 9 }; @@ -150,6 +150,7 @@ bool triggerbot_apply(CUserCmd *cmd); bool silent_aim_apply(CUserCmd *cmd); bool melee_weapon_active(void *local_ent); bool melee_aimbot_apply(CUserCmd *cmd); +bool spread_control_apply(CUserCmd *cmd, void *local_ent); enum FeatureId { FEAT_BHOP = 0, FEAT_ABH = 1, @@ -158,7 +159,8 @@ enum FeatureId { FEAT_TRIGGERBOT = 4, FEAT_SILENTAIM = 5, FEAT_JUMPBUG = 6, - FEAT_MELEE = 7 + FEAT_MELEE = 7, + FEAT_SPREAD = 8 }; enum EspOption { ESP_OPT_SHOW_NEUTRAL = 0, @@ -193,6 +195,7 @@ bool feature_bind_held(int feature); bool esp_option(int opt); bool bhop_autostrafe_enabled(); bool jumpbug_assist_enabled(); +bool spread_no_spread_enabled(); unsigned aimbot_bone_mask(); int aimbot_smooth_step(); bool aimbot_vischeck_enabled(); diff --git a/src/menu.cpp b/src/menu.cpp index f590d69..9dffc9e 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -40,7 +40,7 @@ struct MenuNode { const char *label; bool expanded; // for root/feature: whether children are visible int child_count; - MenuNode *children[8]; // max children per node + MenuNode *children[12]; // max children per node // leaf data bool *active_ptr; // NODE_TOGGLE: pointer to the active flag @@ -65,6 +65,8 @@ static FeatureState g_abh{true, 0}; static FeatureState g_jumpbug{true, 0}; static bool g_jumpbug_assist = false; static FeatureState g_melee{true, 0}; +static FeatureState g_spread{true, 0}; +static bool g_spread_no_spread = true; 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 @@ -104,6 +106,9 @@ static MenuNode g_jumpbug_active{NODE_TOGGLE, "Active", false, 0, {}, &g_jumpbug static MenuNode g_jumpbug_assist_node{NODE_TOGGLE, "Assist timing", false, 0, {}, &g_jumpbug_assist, nullptr, nullptr}; static MenuNode g_melee_feat{NODE_FEATURE, "Melee killbot", false, 1, {}, nullptr, nullptr, nullptr}; static MenuNode g_melee_active{NODE_TOGGLE, "Active", false, 0, {}, &g_melee.active, nullptr, nullptr}; +static MenuNode g_spread_feat{NODE_FEATURE, "Spread control", false, 1, {}, nullptr, nullptr, nullptr}; +static MenuNode g_spread_active{NODE_TOGGLE, "Active", false, 0, {}, &g_spread.active, nullptr, nullptr}; +static MenuNode g_spread_no_spread_node{NODE_TOGGLE, "No spread", false, 0, {}, &g_spread_no_spread, 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] = { @@ -161,6 +166,9 @@ static void build_tree() { g_jumpbug_feat.child_count = 2; g_melee_feat.children[0] = &g_melee_active; g_melee_feat.child_count = 1; + g_spread_feat.children[0] = &g_spread_active; + g_spread_feat.children[1] = &g_spread_no_spread_node; + g_spread_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; @@ -187,11 +195,12 @@ static void build_tree() { g_root.children[1] = &g_abh_feat; g_root.children[2] = &g_jumpbug_feat; g_root.children[3] = &g_melee_feat; - g_root.children[4] = &g_esp_feat; - g_root.children[5] = &g_aimbot_feat; - g_root.children[6] = &g_triggerbot_feat; - g_root.children[7] = &g_silentaim_feat; - g_root.child_count = 8; + g_root.children[4] = &g_spread_feat; + g_root.children[5] = &g_esp_feat; + g_root.children[6] = &g_aimbot_feat; + g_root.children[7] = &g_triggerbot_feat; + g_root.children[8] = &g_silentaim_feat; + g_root.child_count = 9; } static int g_depth[48]; // depth of each visible row (for indentation) @@ -316,6 +325,8 @@ static void config_save() { fprintf(f, "[jumpbug]\nactive=%d\nassist=%d\n", g_jumpbug.active, (int)g_jumpbug_assist); fprintf(f, "[melee]\nactive=%d\n", g_melee.active); + fprintf(f, "[spread]\nactive=%d\nno_spread=%d\n", g_spread.active, + (int)g_spread_no_spread); 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" @@ -346,7 +357,7 @@ static void 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_jumpbug = false, in_spread = false; while (fgets(line, sizeof(line), f)) { line[strcspn(line, "\r\n")] = 0; if (line[0] == '[') { @@ -358,6 +369,8 @@ static void config_load() { cur = &g_jumpbug; else if (strstr(line, "[melee]")) cur = &g_melee; + else if (strstr(line, "[spread]")) + cur = &g_spread; else if (strstr(line, "[esp]")) cur = &g_esp; else if (strstr(line, "[aimbot]")) @@ -373,6 +386,7 @@ static void config_load() { in_aim = (cur == &g_aimbot); in_silent = (cur == &g_silentaim); in_jumpbug = (cur == &g_jumpbug); + in_spread = (cur == &g_spread); } else if (cur) { char *eq = strchr(line, '='); if (!eq) @@ -385,6 +399,9 @@ static void config_load() { if (in_jumpbug && strcmp(key, "assist") == 0) { g_jumpbug_assist = atoi(val) != 0; } + if (in_spread && strcmp(key, "no_spread") == 0) { + g_spread_no_spread = atoi(val) != 0; + } if (in_aim) { if (strcmp(key, "smooth") == 0) { int v = atoi(val); @@ -546,10 +563,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[8] = {&g_bhop, &g_abh, &g_esp, - &g_aimbot, &g_triggerbot, &g_silentaim, - &g_jumpbug, &g_melee}; - if (feature < 0 || feature > 7) + 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) return nullptr; return feats[feature]; } @@ -579,6 +596,8 @@ bool bhop_autostrafe_enabled() { return g_bhop_autostrafe; } bool jumpbug_assist_enabled() { return g_jumpbug_assist; } +bool spread_no_spread_enabled() { return g_spread_no_spread; } + unsigned aimbot_bone_mask() { unsigned m = 0; if (g_aim_bones[AIM_BONE_HEAD]) -- cgit v1.2.3