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.
Diffstat (limited to '')
| -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; |
