tools(re): walk the record pool embedded in the club-model owner

Records are 0x180 bytes starting at owner+0x162d8, below the embedded
manager subobject at owner+0x1f9d8. Walking that pool answers whether the
client ever builds a record for an item it was served, independently of
whether the record is filed into a collection.

Result on the live client with the kit selector open: 21 records exist -
18 squad players with category 1, plus one cardtype 10 and two cardtype 4
staff with category 0 - and no cardtype 7 record anywhere, despite two kit
items having been served three times in the same boot.
This commit is contained in:
funman300
2026-08-24 17:00:31 +00:00
parent 8983998707
commit 25b7089c54
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Read-only scan of the record pool embedded in the club-model owner object.
The 18 resident player records sit at a fixed stride of 0x180 inside the owner
object, below the embedded manager subobject at owner+0x1f9d8. This walks that
pool to see whether storage for the five club items exists and what it holds.
Read-only. Never writes.
"""
import re, struct, sys
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]
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"))
def live(s):
return base + (s - 0x180000000)
owner = q(live(0x1802e6398))
mgr = owner + 0x1f9d8
beg, end = q(mgr + 0xd8), q(mgr + 0xe0)
first = None
for k in range((end - beg) // 24):
r = q(beg + k * 24 + 0x10)
if r:
first = r; break
if first is None:
sys.exit("no populated player record to anchor the pool")
print(f" owner = {owner:#x} mgr = {mgr:#x} first record = {first:#x}")
print(f" record - owner = {first - owner:#x} pool room to mgr = {(mgr - first) // 0x180} slots of 0x180")
print()
hdr = f" {'idx':>3} {'addr':>12} {'ctype':>6} {'subtyp':>6} {'state':>6} {'cat':>4} {'team':>5} {'kittyp':>6} set"
print(hdr)
n = (mgr - first) // 0x180
for k in range(min(n, 40)):
a = first + k * 0x180
try:
r = rd(a, 0xC0)
except OSError:
print(f" {k:>3} {a:#12x} unreadable"); break
if len(r) < 0xC0:
break
ct, sub, st, cat, team = (struct.unpack_from("<i", r, o)[0] for o in (0x4c, 0x50, 0x5c, 0x60, 0x94))
kt = struct.unpack_from("<H", r, 0xba)[0]
nz = sum(1 for b in r if b)
flag = ""
if ct == 7:
flag = " <== CARDTYPE 7"
elif nz == 0:
flag = " (all zero)"
print(f" {k:>3} {a:#12x} {ct:>6} {sub:>6} {st:>6} {cat:>4} {team:>5} {kt:>6} {nz:>3}/192{flag}")