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 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) (limited to 'src/esp.cpp') 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; -- cgit v1.2.3