summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-04 13:26:33 +0100
committersalmonik <salmonik@tuta.io>2026-11-04 13:26:33 +0100
commitd22bd8d184091e6a4600beb78eb279c2f5fab51b (patch)
treed2599374c611b56a5568f949b93e7fab2a93f6c8
parentFix No spread on the hl2_complete server build (diff)
downloadhl2h-d22bd8d184091e6a4600beb78eb279c2f5fab51b.tar.gz
hl2h-d22bd8d184091e6a4600beb78eb279c2f5fab51b.zip
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.
Diffstat (limited to '')
-rw-r--r--CONTEXT.md8
-rw-r--r--src/hl2h.cpp64
2 files changed, 72 insertions, 0 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index bd3507a..75b2f0a 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -298,6 +298,14 @@ vocabulary / decisions.
298 `CBaseEntity::FireBullets`; the `CUniformRandomStream` wrappers and 298 `CBaseEntity::FireBullets`; the `CUniformRandomStream` wrappers and
299 constructor-time `RandomSeed(0)` resets pass other objects and are left 299 constructor-time `RandomSeed(0)` resets pass other objects and are left
300 alone. 300 alone.
301- Seed choice alone cannot remove all of a shotgun's pellet spread: the best
302 of 256 sequences still leaves its 8/12 draws apart. `CShotManipulator::
303 ApplySpread` is inlined into `CBaseEntity::FireBullets` and draws x/y via
304 the global `CUniformRandomStream` (`_ZTV20CUniformRandomStream` + 8, slot 1
305 = `RandomFloat(float,float)`; confirmed against `libvstdlib.so`). The hook
306 returns 0 for those draws only when the calling frame's `this` (at
307 `ebp+8`) is the local server player, so every pellet follows the exact shot
308 direction. NPC fire and the client's prediction stream keep normal values.
301- This attacks the random cone offset only. The cone size, the pistol's 309- This attacks the random cone offset only. The cone size, the pistol's
302 `m_flAccuracyPenalty` growth, the server view punch added to 310 `m_flAccuracyPenalty` growth, the server view punch added to
303 `GetAutoaimVector`, and any damage falloff are server-side state. In 311 `GetAutoaimVector`, and any damage falloff are server-side state. In
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() {
574 (void *)g_real_random_seed); 574 (void *)g_real_random_seed);
575} 575}
576 576
577
578// ---------------------------------------------------------------------------
579// pellet spread: zero the random offsets inside FireBullets
580// ---------------------------------------------------------------------------
581//
582// CShotManipulator::ApplySpread is inlined into CBaseEntity::FireBullets and
583// draws its x/y offsets through the global CUniformRandomStream (vtable slot
584// 1, confirmed against libvstdlib: _ZTV20CUniformRandomStream + 8 + 4). The
585// seed search can only choose the least-bad sequence, which still leaves the
586// shotgun's pellets slightly apart. Returning 0 for these draws makes every
587// pellet follow the shot direction exactly. The hook is conditional on the
588// calling frame belonging to the local server player, so NPC fire and the
589// client's own prediction stream keep their randomness.
590
591static float (*g_orig_uniform_float)(void *, float, float) = nullptr;
592static bool g_uniform_hook_installed = false;
593
594static float hkdUniformRandomFloat(void *self, float mn, float mx) {
595 if (g_local_server_player && feature_active(FEAT_SPREAD) &&
596 spread_no_spread_enabled()) {
597 void *frame = nullptr;
598 __asm__ volatile("movl %%ebp, %0" : "=r"(frame));
599 if (frame) {
600 frame = *(void **)frame; // caller's ebp (CBaseEntity::FireBullets)
601 void *who = frame ? *(void **)((char *)frame + 8) : nullptr;
602 if (who && (uintptr_t)who == g_local_server_player)
603 return 0.0f;
604 }
605 }
606 return g_orig_uniform_float ? g_orig_uniform_float(self, mn, mx) : mn;
607}
608
609static void install_uniform_random_hook() {
610 if (g_uniform_hook_installed)
611 return;
612 void *vt = dlsym(RTLD_DEFAULT, "_ZTV20CUniformRandomStream");
613 if (!vt) {
614 logf("spread: CUniformRandomStream vtable not found");
615 return;
616 }
617 void **ap = (void **)((char *)vt + 8); // skip offset-to-top and typeinfo
618 void *rf = dlsym(RTLD_DEFAULT, "_ZN20CUniformRandomStream11RandomFloatEff");
619 int slot = 1; // RandomFloat(min,max) follows SetSeed in the vtable
620 if (rf) {
621 for (int i = 0; i < 8; i++) {
622 if (ap[i] == rf) {
623 slot = i;
624 break;
625 }
626 }
627 }
628 g_orig_uniform_float = (float (*)(void *, float, float))ap[slot];
629 uintptr_t page = (uintptr_t)&ap[slot] & ~(uintptr_t)0xFFF;
630 if (mprotect((void *)page, 0x1000, PROT_READ | PROT_WRITE) != 0) {
631 logf("spread: could not make the random vtable writable");
632 return;
633 }
634 ap[slot] = (void *)&hkdUniformRandomFloat;
635 g_uniform_hook_installed = true;
636 logf("spread: CUniformRandomStream::RandomFloat slot %d (%p) -> %p", slot,
637 (void *)g_orig_uniform_float, (void *)&hkdUniformRandomFloat);
638}
639
577static void do_move(int seq, float frametime) { 640static void do_move(int seq, float frametime) {
578 install_seed_hook(); 641 install_seed_hook();
642 install_uniform_random_hook();
579 static bool g_echo_sent = false; 643 static bool g_echo_sent = false;
580 if (!g_input || !g_net.ok()) 644 if (!g_input || !g_net.ok())
581 return; 645 return;