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 | |
| 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.
| -rw-r--r-- | CONTEXT.md | 8 | ||||
| -rw-r--r-- | src/hl2h.cpp | 64 |
2 files changed, 72 insertions, 0 deletions
| @@ -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 | |||
| 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; |
