summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CONTEXT.md15
-rw-r--r--src/hl2h.cpp50
-rw-r--r--src/hl2h.h2
-rw-r--r--src/modules.cpp99
-rw-r--r--src/server.cpp11
5 files changed, 157 insertions, 20 deletions
diff --git a/CONTEXT.md b/CONTEXT.md
index 5e36328..bd3507a 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -205,8 +205,10 @@ vocabulary / decisions.
205- Aimbot/silent aim/triggerbot/melee skip `IMMUNE` targets always and 205- Aimbot/silent aim/triggerbot/melee skip `IMMUNE` targets always and
206 `ARMORED` targets unless the active weapon is the RPG 206 `ARMORED` targets unless the active weapon is the RPG
207 (`active_weapon_contains` for `rocket_launcher`/`rpg`). 207 (`active_weapon_contains` for `rocket_launcher`/`rpg`).
208- Server `m_takedamage` (`FIELD_INTEGER`) is resolved alongside 208- Server `m_takedamage` is a `char` in the 2013 SDK
209 `m_NPCState`; `0/1` means no damage. 209 (`CNetworkVarForDerived(char, m_takedamage)`, `FIELD_CHARACTER`), so it is
210 resolved with that type (int fallback) and read as a byte. `0/1` means no
211 damage.
210 212
211## Bones / ESP 213## Bones / ESP
212 214
@@ -287,6 +289,15 @@ vocabulary / decisions.
287 `FIRE_BULLETS_FIRST_SHOT_ACCURATE`, so the table excludes it; the secondary 289 `FIRE_BULLETS_FIRST_SHOT_ACCURATE`, so the table excludes it; the secondary
288 uses all 12. Separate best starting seeds are precomputed for 1, 8 and 12 290 uses all 12. Separate best starting seeds are precomputed for 1, 8 and 12
289 pellets. 291 pellets.
292- The server `RandomSeed` PLT slot is found at runtime by reading the loaded
293 `server.so` ELF relocation table (`module_jump_slot`): the anniversary
294 branch's `hl2_complete/bin/server.so` moved the `R_386_JMP_SLOT` from
295 `0xce1348` (older `bin/server.so`) to `0xce5348`, and the hardcoded patch
296 silently hit unrelated data. The hook overrides the seed whenever the
297 `RandomSeed` caller's `this` is the local server player, i.e.
298 `CBaseEntity::FireBullets`; the `CUniformRandomStream` wrappers and
299 constructor-time `RandomSeed(0)` resets pass other objects and are left
300 alone.
290- This attacks the random cone offset only. The cone size, the pistol's 301- This attacks the random cone offset only. The cone size, the pistol's
291 `m_flAccuracyPenalty` growth, the server view punch added to 302 `m_flAccuracyPenalty` growth, the server view punch added to
292 `GetAutoaimVector`, and any damage falloff are server-side state. In 303 `GetAutoaimVector`, and any damage falloff are server-side state. In
diff --git a/src/hl2h.cpp b/src/hl2h.cpp
index 28b2995..7ce35d3 100644
--- a/src/hl2h.cpp
+++ b/src/hl2h.cpp
@@ -471,11 +471,12 @@ static bool jumpbug_apply(CUserCmd *cmd, int local, void *ent, int flags,
471// server RandomSeed diagnostic / override 471// server RandomSeed diagnostic / override
472// --------------------------------------------------------------------------- 472// ---------------------------------------------------------------------------
473// 473//
474// The installed 2024 server build is stripped and its usercmd/server-random-seed 474// The server is a stripped build whose usercmd/random-seed handling differs
475// handling differs from the 2013 SDK source. This hook is installed on the 475// from the 2013 SDK source. Hook the server.so PLT slot for RandomSeed so we
476// server.so GOT slot for RandomSeed (R_386_JUMP_SLOT at vaddr 0xce1348) so we 476// can force the chosen spread seed. The GOT offset is read from the loaded
477// can see which seed FireBullets actually uses and optionally force the chosen 477// module's relocation table at runtime (module_jump_slot): hardcoding it broke
478// spread seed. Disabled unless HL2H_SEED_TRACE / HL2H_SEED_FORCE are set. 478// when Steam switched to the hl2_complete/server.so branch (0xce1348 ->
479// 0xce5348).
479 480
480static void (*g_real_random_seed)(int) = nullptr; 481static void (*g_real_random_seed)(int) = nullptr;
481static uintptr_t g_srv_base = 0; 482static uintptr_t g_srv_base = 0;
@@ -487,8 +488,10 @@ static void hkdSrvRandomSeed(int seed) {
487 long off = (long)(ret - g_srv_base); 488 long off = (long)(ret - g_srv_base);
488 static bool trace = getenv("HL2H_SEED_TRACE") != nullptr; 489 static bool trace = getenv("HL2H_SEED_TRACE") != nullptr;
489 static int logs = 0; 490 static int logs = 0;
490 bool bullet_site = (off >= 0x216900 && off <= 0x216930);
491 491
492 // The caller's first argument is `this`. Only FireBullets is called on the
493 // player entity; the CUniformRandomStream wrappers and the constructor-time
494 // RandomSeed(0) reset have a different object there.
492 void *frame = nullptr; 495 void *frame = nullptr;
493 __asm__ volatile("movl %%ebp, %0" : "=r"(frame)); 496 __asm__ volatile("movl %%ebp, %0" : "=r"(frame));
494 if (frame) 497 if (frame)
@@ -497,7 +500,7 @@ static void hkdSrvRandomSeed(int seed) {
497 bool local = self && g_local_server_player && 500 bool local = self && g_local_server_player &&
498 (uintptr_t)self == g_local_server_player; 501 (uintptr_t)self == g_local_server_player;
499 502
500 if (bullet_site && local && feature_active(FEAT_SPREAD) && 503 if (local && g_server.contains(ret) && feature_active(FEAT_SPREAD) &&
501 spread_no_spread_enabled()) { 504 spread_no_spread_enabled()) {
502 int pellets = g_spread_pellets; 505 int pellets = g_spread_pellets;
503 if (pellets < 1) 506 if (pellets < 1)
@@ -516,12 +519,10 @@ static void hkdSrvRandomSeed(int seed) {
516 idx++; 519 idx++;
517 } 520 }
518 521
519 if (trace && (bullet_site || logs < 12)) { 522 if (trace && logs < 200) {
520 if (logs < 200) { 523 logs++;
521 logs++; 524 logd("srvseed: seed=%d off=0x%lx self=%p local=%d", seed, off, self,
522 logd("srvseed: seed=%d off=0x%lx bullet=%d self=%p local=%d", seed, off, 525 (int)local);
523 (int)bullet_site, self, (int)local);
524 }
525 } 526 }
526 if (g_real_random_seed) 527 if (g_real_random_seed)
527 g_real_random_seed(seed); 528 g_real_random_seed(seed);
@@ -532,9 +533,28 @@ static void install_seed_hook() {
532 if (g_seed_hook_installed || failed || !g_server.valid()) 533 if (g_seed_hook_installed || failed || !g_server.valid())
533 return; 534 return;
534 g_srv_base = g_server.ranges[0].lo; 535 g_srv_base = g_server.ranges[0].lo;
535 uintptr_t slot = g_srv_base + 0xce1348; // R_386_JUMP_SLOT RandomSeed 536 uintptr_t reloc = 0, bias = 0;
536 if (!g_server.contains(slot)) 537 uintptr_t slot = 0;
538 if (module_jump_slot(g_server.path.c_str(), "RandomSeed", &reloc, &bias)) {
539 slot = g_srv_base + reloc - bias;
540 logf("seed hook: RandomSeed GOT reloc=%#lx bias=%#lx -> %#lx",
541 (unsigned long)reloc, (unsigned long)bias, (unsigned long)slot);
542 } else {
543 // ELF parse failed (deleted/unreadable module); try the builds we know.
544 static const uintptr_t known[] = {0xce5348, 0xce1348};
545 for (size_t i = 0; i < sizeof(known) / sizeof(known[0]) && !slot; i++) {
546 uintptr_t cand = g_srv_base + known[i];
547 if (g_server.contains(cand))
548 slot = cand;
549 }
550 logf("seed hook: ELF reloc scan failed, fallback slot=%#lx",
551 (unsigned long)slot);
552 }
553 if (!slot || !g_server.contains(slot)) {
554 failed = true;
555 logf("seed hook: no writable RandomSeed slot");
537 return; 556 return;
557 }
538 g_real_random_seed = 558 g_real_random_seed =
539 (void (*)(int))dlsym(RTLD_NEXT, "RandomSeed"); 559 (void (*)(int))dlsym(RTLD_NEXT, "RandomSeed");
540 if (!g_real_random_seed) 560 if (!g_real_random_seed)
diff --git a/src/hl2h.h b/src/hl2h.h
index e35e737..b7fc2f5 100644
--- a/src/hl2h.h
+++ b/src/hl2h.h
@@ -278,6 +278,8 @@ int server_npc_takedamage(int idx);
278int server_npc_turret_flags(int idx); 278int server_npc_turret_flags(int idx);
279int server_model_index(void *base); 279int server_model_index(void *base);
280void find_modules(); 280void find_modules();
281bool module_jump_slot(const char *path, const char *name, uintptr_t *vaddr,
282 uintptr_t *bias);
281void init_conmsg(); 283void init_conmsg();
282bool feature_active(int feature); 284bool feature_active(int feature);
283bool feature_has_bind(int feature); 285bool feature_has_bind(int feature);
diff --git a/src/modules.cpp b/src/modules.cpp
index 70e2dff..b832694 100644
--- a/src/modules.cpp
+++ b/src/modules.cpp
@@ -1,5 +1,7 @@
1// modules.cpp — /proc/self/maps parsing and module discovery 1// modules.cpp — /proc/self/maps parsing and module discovery
2#include "hl2h.h" 2#include "hl2h.h"
3#include <elf.h>
4#include <vector>
3 5
4static bool basename_is(const char *path, const char *base) { 6static bool basename_is(const char *path, const char *base) {
5 size_t pl = strlen(path), bl = strlen(base); 7 size_t pl = strlen(path), bl = strlen(base);
@@ -70,3 +72,100 @@ void find_modules() {
70 } 72 }
71 fclose(f); 73 fclose(f);
72} 74}
75
76// ---------------------------------------------------------------------------
77// ELF relocation lookup
78// ---------------------------------------------------------------------------
79//
80// The server RandomSeed hook used to hardcode the GOT offset measured on one
81// server.so build. The anniversary branch ships a different server.so
82// (hl2_complete/bin/server.so) whose slot moved, so the patch silently landed
83// on unrelated data and No spread stopped working. Read the loaded module's
84// ELF relocation table instead: find the R_386_JMP_SLOT for a symbol and
85// return its virtual address plus the load bias implied by the first PT_LOAD.
86bool module_jump_slot(const char *path, const char *name, uintptr_t *out_vaddr,
87 uintptr_t *out_bias) {
88 if (!path || !*path || !name)
89 return false;
90 FILE *f = fopen(path, "rb");
91 if (!f)
92 return false;
93 if (fseek(f, 0, SEEK_END) != 0) {
94 fclose(f);
95 return false;
96 }
97 long sz = ftell(f);
98 if (sz < (long)sizeof(Elf32_Ehdr) || fseek(f, 0, SEEK_SET) != 0) {
99 fclose(f);
100 return false;
101 }
102 std::vector<unsigned char> buf((size_t)sz);
103 size_t got = fread(buf.data(), 1, buf.size(), f);
104 fclose(f);
105 if (got != buf.size())
106 return false;
107 if (buf.size() < sizeof(Elf32_Ehdr))
108 return false;
109
110 const Elf32_Ehdr *eh = (const Elf32_Ehdr *)buf.data();
111 if (memcmp(eh->e_ident, ELFMAG, SELFMAG) != 0 ||
112 eh->e_ident[EI_CLASS] != ELFCLASS32)
113 return false;
114 if (eh->e_shoff == 0 || eh->e_shentsize != sizeof(Elf32_Shdr))
115 return false;
116 if ((uint64_t)eh->e_shoff + (uint64_t)eh->e_shnum * sizeof(Elf32_Shdr) >
117 buf.size())
118 return false;
119 const Elf32_Shdr *sh = (const Elf32_Shdr *)(buf.data() + eh->e_shoff);
120
121 uintptr_t bias = 0;
122 if (eh->e_phoff && eh->e_phnum &&
123 eh->e_phentsize == sizeof(Elf32_Phdr) &&
124 (uint64_t)eh->e_phoff + (uint64_t)eh->e_phnum * sizeof(Elf32_Phdr) <=
125 buf.size()) {
126 const Elf32_Phdr *ph = (const Elf32_Phdr *)(buf.data() + eh->e_phoff);
127 for (int i = 0; i < eh->e_phnum; i++) {
128 if (ph[i].p_type == PT_LOAD) {
129 bias = ph[i].p_vaddr;
130 break;
131 }
132 }
133 }
134
135 for (int s = 0; s < eh->e_shnum; s++) {
136 if (sh[s].sh_type != SHT_REL || sh[s].sh_link >= eh->e_shnum)
137 continue;
138 if ((uint64_t)sh[s].sh_offset + (uint64_t)sh[s].sh_size > buf.size())
139 continue;
140 const Elf32_Shdr &symsh = sh[sh[s].sh_link];
141 if (symsh.sh_link >= eh->e_shnum)
142 continue;
143 const Elf32_Shdr &strsh = sh[symsh.sh_link];
144 if ((uint64_t)symsh.sh_offset + (uint64_t)symsh.sh_size > buf.size() ||
145 (uint64_t)strsh.sh_offset + (uint64_t)strsh.sh_size > buf.size())
146 continue;
147 const Elf32_Rel *rels = (const Elf32_Rel *)(buf.data() + sh[s].sh_offset);
148 size_t nrels = sh[s].sh_size / sizeof(Elf32_Rel);
149 const Elf32_Sym *syms = (const Elf32_Sym *)(buf.data() + symsh.sh_offset);
150 size_t nsyms = symsh.sh_size / sizeof(Elf32_Sym);
151 const char *strs = (const char *)(buf.data() + strsh.sh_offset);
152 size_t strn = strsh.sh_size;
153 for (size_t r = 0; r < nrels; r++) {
154 unsigned type = ELF32_R_TYPE(rels[r].r_info);
155 unsigned sym = ELF32_R_SYM(rels[r].r_info);
156 if (type != R_386_JMP_SLOT || sym >= nsyms)
157 continue;
158 if (syms[sym].st_name >= strn)
159 continue;
160 const char *sn = strs + syms[sym].st_name;
161 if (strcmp(sn, name) != 0)
162 continue;
163 if (out_vaddr)
164 *out_vaddr = rels[r].r_offset;
165 if (out_bias)
166 *out_bias = bias;
167 return true;
168 }
169 }
170 return false;
171}
diff --git a/src/server.cpp b/src/server.cpp
index e9fd921..f71b441 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -142,7 +142,11 @@ bool resolve_server_netvars() {
142 g_srv_spawnflags = datamap_offset("m_spawnflags", FIELD_INTEGER, 0x4); 142 g_srv_spawnflags = datamap_offset("m_spawnflags", FIELD_INTEGER, 0x4);
143 g_srv_hacked = datamap_offset("m_bHackedByAlyx", FIELD_BOOLEAN); 143 g_srv_hacked = datamap_offset("m_bHackedByAlyx", FIELD_BOOLEAN);
144 g_srv_npcstate = datamap_offset("m_NPCState", FIELD_INTEGER); 144 g_srv_npcstate = datamap_offset("m_NPCState", FIELD_INTEGER);
145 g_srv_takedamage = datamap_offset("m_takedamage", FIELD_INTEGER); 145 // CBaseEntity::m_takedamage is a char in the 2013 SDK
146 // (CNetworkVarForDerived(char, m_takedamage)); older builds used int.
147 g_srv_takedamage = datamap_offset("m_takedamage", FIELD_CHARACTER);
148 if (g_srv_takedamage < 0)
149 g_srv_takedamage = datamap_offset("m_takedamage", FIELD_INTEGER);
146 g_srv_ready = g_srv_health > 0; 150 g_srv_ready = g_srv_health > 0;
147 if (g_srv_ready) { 151 if (g_srv_ready) {
148 logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d " 152 logf("server datamap (%s): health=%d maxhealth=%d lifeste=%d "
@@ -272,7 +276,8 @@ int server_npc_state(int idx) {
272} 276}
273 277
274// TAKEDAMAGE: 0 = DAMAGE_NO, 1 = DAMAGE_EVENTS_ONLY, 2 = DAMAGE_YES, 278// TAKEDAMAGE: 0 = DAMAGE_NO, 1 = DAMAGE_EVENTS_ONLY, 2 = DAMAGE_YES,
275// 3 = DAMAGE_AIM. 0/1 cannot be damaged at all. 279// 3 = DAMAGE_AIM. 0/1 cannot be damaged at all. The field is a char, so read
280// a byte even if the older int layout was resolved.
276int server_npc_takedamage(int idx) { 281int server_npc_takedamage(int idx) {
277 if (!g_srv_ready || g_srv_takedamage < 0) 282 if (!g_srv_ready || g_srv_takedamage < 0)
278 return -1; 283 return -1;
@@ -280,7 +285,7 @@ int server_npc_takedamage(int idx) {
280 server_entity_health(idx, nullptr, nullptr, &base); 285 server_entity_health(idx, nullptr, nullptr, &base);
281 if (!base) 286 if (!base)
282 return -1; 287 return -1;
283 return *(int *)((uintptr_t)base + g_srv_takedamage); 288 return (int)*(unsigned char *)((uintptr_t)base + g_srv_takedamage);
284} 289}
285 290
286bool server_npc_has_follow_target(int idx) { 291bool server_npc_has_follow_target(int idx) {