summaryrefslogtreecommitdiffstats
path: root/src
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 /src
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 'src')
-rw-r--r--src/hl2h.cpp64
1 files changed, 64 insertions, 0 deletions
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;