diff options
| author | salmonik <salmonik@tuta.io> | 2026-11-04 13:26:33 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-11-04 13:26:33 +0100 |
| commit | d22bd8d184091e6a4600beb78eb279c2f5fab51b (patch) | |
| tree | d2599374c611b56a5568f949b93e7fab2a93f6c8 /src/hl2h.cpp | |
| parent | Fix No spread on the hl2_complete server build (diff) | |
| download | hl2h-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/hl2h.cpp')
| -rw-r--r-- | src/hl2h.cpp | 64 |
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 | |||
| 591 | static float (*g_orig_uniform_float)(void *, float, float) = nullptr; | ||
| 592 | static bool g_uniform_hook_installed = false; | ||
| 593 | |||
| 594 | static 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 | |||
| 609 | static 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 | |||
| 577 | static void do_move(int seq, float frametime) { | 640 | static 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; |
