42229e5782
The squad parser writes actives element i to club-item slot r15d+i, and r15d is shared scratch that other atom handlers clobber. Which slots are filled therefore reveals the index the parse actually started from, which is the open question behind the regression in vault section 18. probe_resident_fields.py now prints the slot index of every populated entry plus the empty ones, and verify_kits.sh runs it next to the map census so one command reports both residency and whether the squad survived.
114 lines
4.3 KiB
Python
Executable File
114 lines
4.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Read-only dump of RESIDENT record fields, for both the player and club-item vectors.
|
|
|
|
Purpose: the kit clone driver FUN_1801c3480 gates on record+0x60 (category) == 4.
|
|
No instruction in CardsDLL writes immediate 4 there, so this reads what value a
|
|
genuinely resident record actually carries. Read-only. Never writes.
|
|
|
|
mgr+0x0c0 cardtype-2 single slot
|
|
mgr+0x0d8..0x0e0 cardtype-1 (player) vector
|
|
mgr+0x108..0x110 club-item vector
|
|
record+0x4c cardtype +0x50 cardsubtypeid +0x5c itemState
|
|
record+0x60 category +0x94 teamid +0xba teamkittypetechid (u16)
|
|
"""
|
|
import re, struct, sys, collections
|
|
|
|
PID = int(sys.argv[1])
|
|
mem = open(f"/proc/{PID}/mem", "rb", buffering=0)
|
|
|
|
def rd(a, n):
|
|
mem.seek(a); return mem.read(n)
|
|
def q(a):
|
|
return struct.unpack("<Q", rd(a, 8))[0]
|
|
def i32(b, o):
|
|
return struct.unpack_from("<i", b, o)[0]
|
|
|
|
named = []
|
|
for ln in open(f"/proc/{PID}/maps"):
|
|
m = re.match(r"([0-9a-f]+)-([0-9a-f]+) \S{4} \S+ \S+ \S+\s+(.+)", ln)
|
|
if m:
|
|
named.append((int(m.group(1), 16), m.group(3).strip()))
|
|
named.sort()
|
|
base = next((s for s, p in named if p.endswith("CardsDLL_Win64_retail.dll")), None)
|
|
if base is None:
|
|
sys.exit("CardsDLL mapping not found")
|
|
def live(static):
|
|
return base + (static - 0x180000000)
|
|
|
|
if rd(live(0x180026fea), 5) != bytes.fromhex("ba75750000"):
|
|
sys.exit("SANITY FAILED - wrong base")
|
|
print(f" CardsDLL base = {base:#x} (sanity ok)")
|
|
|
|
owner = q(live(0x1802e6398))
|
|
b = rd(q(owner) + 0x4e8, 12)
|
|
b = rd(struct.unpack("<Q", struct.pack("<Q", q(q(owner) + 0x4e8)))[0], 12)
|
|
getter = q(q(owner) + 0x4e8)
|
|
gb = rd(getter, 12)
|
|
if gb[0:3] == bytes.fromhex("488d81"):
|
|
mgr = owner + struct.unpack_from("<I", gb, 3)[0]
|
|
elif gb[0:3] == bytes.fromhex("488d41"):
|
|
mgr = owner + gb[3]
|
|
else:
|
|
sys.exit(f"unexpected getter shape {gb.hex(' ')}")
|
|
print(f" owner = {owner:#x} mgr = {mgr:#x}")
|
|
|
|
FIELDS = ("ctype", "subtype", "state", "cat", "team", "kittype")
|
|
def decode(rec):
|
|
r = rd(rec, 0xC0)
|
|
if len(r) < 0xC0:
|
|
return None
|
|
return (i32(r, 0x4c), i32(r, 0x50), i32(r, 0x5c), i32(r, 0x60),
|
|
i32(r, 0x94), struct.unpack_from("<H", r, 0xba)[0])
|
|
|
|
for label, vbeg, vend in (("players (cardtype 1)", mgr + 0xd8, mgr + 0xe0),
|
|
("club items", mgr + 0x108, mgr + 0x110)):
|
|
try:
|
|
beg, end = q(vbeg), q(vend)
|
|
except OSError:
|
|
print(f"\n {label}: vector unreadable")
|
|
continue
|
|
span = end - beg
|
|
print(f"\n === {label}: {beg:#x}..{end:#x} span={span} ===")
|
|
if not (0 < beg <= end) or span > 24 * 100000:
|
|
print(" implausible vector, skipping")
|
|
continue
|
|
# resolve stride: the element must contain a plausible heap pointer
|
|
for stride, ptr_off in ((24, 0x10), (16, 0x08), (8, 0x00)):
|
|
if span % stride:
|
|
continue
|
|
n = span // stride
|
|
recs, nulls = [], []
|
|
ok = True
|
|
for k in range(n):
|
|
try:
|
|
rec = q(beg + k * stride + ptr_off)
|
|
except OSError:
|
|
ok = False; break
|
|
if not rec:
|
|
nulls.append(k); continue
|
|
d = decode(rec)
|
|
if d is None:
|
|
ok = False; break
|
|
recs.append((k, rec, d))
|
|
if not ok:
|
|
continue
|
|
print(f" stride {stride} (ptr at +{ptr_off:#x}): {n} slots, {len(recs)} populated, {len(nulls)} null")
|
|
if not recs and len(nulls) != n:
|
|
continue
|
|
hist = collections.Counter(d[0:2] for _, _, d in recs)
|
|
for key, c in sorted(hist.items(), key=lambda x: -x[1]):
|
|
print(f" (cardtype,subtype)={key} x{c}")
|
|
# The SLOT INDEX is load-bearing evidence: the squad parser's `actives`
|
|
# arm writes element i to slot `r15d + i`, and r15d is shared scratch
|
|
# that other atom handlers clobber. Which slots are filled therefore
|
|
# reveals the index the parse actually started from.
|
|
print(f" {'slot':>4} {'ptr':>14} " + " ".join(f"{f:>8}" for f in FIELDS))
|
|
for k, rec, d in recs[:8]:
|
|
print(f" {k:>4} {rec:#14x} " + " ".join(f"{v:>8}" for v in d))
|
|
if nulls:
|
|
print(f" empty slots: {nulls[:16]}")
|
|
cats = collections.Counter(d[3] for _, _, d in recs)
|
|
if cats:
|
|
print(f" CATEGORY (+0x60) distribution: {dict(cats)}")
|
|
break
|