From d22bd8d184091e6a4600beb78eb279c2f5fab51b Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 4 Nov 2026 13:26:33 +0100 Subject: No spread: zero pellet offsets inside FireBullets Picking the best of 256 pending seeds still leaves the shotgun's 8/12 pellets a few degrees apart. CShotManipulator::ApplySpread is inlined into CBaseEntity::FireBullets and draws x/y through the global CUniformRandomStream; hook its RandomFloat slot and return 0 when the calling frame's shooter is the local server player. This collapses every pellet onto the shot direction while NPC fire and client prediction keep their randomness. --- src/hl2h.cpp | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) (limited to 'src') diff --git a/src/hl2h.cpp b/src/hl2h.cpp index 7ce35d3..02bf584 100644 --- a/src/hl2h.cpp +++ b/src/hl2h.cpp @@ -574,8 +574,72 @@ static void install_seed_hook() { (void *)g_real_random_seed); } + +// --------------------------------------------------------------------------- +// pellet spread: zero the random offsets inside FireBullets +// --------------------------------------------------------------------------- +// +// CShotManipulator::ApplySpread is inlined into CBaseEntity::FireBullets and +// draws its x/y offsets through the global CUniformRandomStream (vtable slot +// 1, confirmed against libvstdlib: _ZTV20CUniformRandomStream + 8 + 4). The +// seed search can only choose the least-bad sequence, which still leaves the +// shotgun's pellets slightly apart. Returning 0 for these draws makes every +// pellet follow the shot direction exactly. The hook is conditional on the +// calling frame belonging to the local server player, so NPC fire and the +// client's own prediction stream keep their randomness. + +static float (*g_orig_uniform_float)(void *, float, float) = nullptr; +static bool g_uniform_hook_installed = false; + +static float hkdUniformRandomFloat(void *self, float mn, float mx) { + if (g_local_server_player && feature_active(FEAT_SPREAD) && + spread_no_spread_enabled()) { + void *frame = nullptr; + __asm__ volatile("movl %%ebp, %0" : "=r"(frame)); + if (frame) { + frame = *(void **)frame; // caller's ebp (CBaseEntity::FireBullets) + void *who = frame ? *(void **)((char *)frame + 8) : nullptr; + if (who && (uintptr_t)who == g_local_server_player) + return 0.0f; + } + } + return g_orig_uniform_float ? g_orig_uniform_float(self, mn, mx) : mn; +} + +static void install_uniform_random_hook() { + if (g_uniform_hook_installed) + return; + void *vt = dlsym(RTLD_DEFAULT, "_ZTV20CUniformRandomStream"); + if (!vt) { + logf("spread: CUniformRandomStream vtable not found"); + return; + } + void **ap = (void **)((char *)vt + 8); // skip offset-to-top and typeinfo + void *rf = dlsym(RTLD_DEFAULT, "_ZN20CUniformRandomStream11RandomFloatEff"); + int slot = 1; // RandomFloat(min,max) follows SetSeed in the vtable + if (rf) { + for (int i = 0; i < 8; i++) { + if (ap[i] == rf) { + slot = i; + break; + } + } + } + g_orig_uniform_float = (float (*)(void *, float, float))ap[slot]; + uintptr_t page = (uintptr_t)&ap[slot] & ~(uintptr_t)0xFFF; + if (mprotect((void *)page, 0x1000, PROT_READ | PROT_WRITE) != 0) { + logf("spread: could not make the random vtable writable"); + return; + } + ap[slot] = (void *)&hkdUniformRandomFloat; + g_uniform_hook_installed = true; + logf("spread: CUniformRandomStream::RandomFloat slot %d (%p) -> %p", slot, + (void *)g_orig_uniform_float, (void *)&hkdUniformRandomFloat); +} + static void do_move(int seq, float frametime) { install_seed_hook(); + install_uniform_random_hook(); static bool g_echo_sent = false; if (!g_input || !g_net.ok()) return; -- cgit v1.2.3