2e97ff1461
Re-entry discriminator came back a CONFIRMED BUG: an unlisted transfer-list item does not survive a fresh FUT session, so our representation cannot reconstruct trade-pile membership. Evidence acquisition per instruction, corpus and PE first, no guessing. Adds route_table_dump.py: static read-only dump of CardsDLL's route table from the on-disk PE, resolving VA->file offset through the real section table instead of assuming a single .text mapping. Output preserved as evidence. It settles "is /tradePile the only relevant route?" -- the table holds 45 routes plus 3 empty admin slots, and row 30 `ut/%s/tradePile` is the ONLY trade-pile route. There is no trade-pile items route. That plus three existing PE facts narrows the representation to exactly one candidate by ELIMINATION rather than choice: the route carries only twelve-atom auction records; `pile` (0x226) has no arm in the item deserializer so membership is conferred by the owning list and cannot be added as a field; of the twelve atoms only tradeState expresses lifecycle; and tradeState's closed vocabulary (active=1 inactive=2 expired=3 closed=4) has exactly one value not already spoken for. So an unlisted item can only be an auctionInfo record with tradeState "inactive". Tagged INFERRED-BY-ELIMINATION, not CONFIRMED: the remaining unknown is whether the Flash Transfer List RENDERS such a record in the unlisted section. Records the acceptance test (survive a full FUT reload) and the revised invariant that a transition is complete only when a fresh session reconstructs the same visible state. No behaviour change in this commit.
77 lines
2.5 KiB
Python
77 lines
2.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Dump CardsDLL's 45-row route table from the ON-DISK PE. READ-ONLY, static.
|
|
|
|
The transfer-market analysis locates the table at .rdata 0x18021df80 as
|
|
{char*, char*} rows. This resolves VA->file offset properly through the PE section
|
|
table rather than assuming a single .text mapping, then prints every row so we can
|
|
see whether any route other than `tradePile` could own a trade-pile ITEM list.
|
|
"""
|
|
import struct, sys
|
|
|
|
DLL = "/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll"
|
|
TABLE_VA = 0x18021DF80
|
|
MAX_ROWS = 64
|
|
|
|
pe = open(DLL, "rb").read()
|
|
e_lfanew = struct.unpack_from("<I", pe, 0x3C)[0]
|
|
assert pe[e_lfanew:e_lfanew + 4] == b"PE\0\0", "not a PE"
|
|
coff = e_lfanew + 4
|
|
nsec, opt_size = struct.unpack_from("<HH", pe, coff + 2), None
|
|
num_sections = struct.unpack_from("<H", pe, coff + 2)[0]
|
|
opt_size = struct.unpack_from("<H", pe, coff + 16)[0]
|
|
opt = coff + 20
|
|
magic = struct.unpack_from("<H", pe, opt)[0]
|
|
assert magic == 0x20B, "expected PE32+"
|
|
image_base = struct.unpack_from("<Q", pe, opt + 24)[0]
|
|
sec_off = opt + opt_size
|
|
|
|
sections = []
|
|
for i in range(num_sections):
|
|
b = sec_off + i * 40
|
|
name = pe[b:b + 8].rstrip(b"\0").decode("ascii", "replace")
|
|
vsize, vaddr, rawsize, rawptr = struct.unpack_from("<IIII", pe, b + 8)
|
|
sections.append((name, vaddr, vsize, rawptr, rawsize))
|
|
|
|
print("image_base=%#x sections=%d" % (image_base, num_sections))
|
|
for s in sections:
|
|
print(" %-8s rva=%#010x vsize=%#x rawptr=%#010x rawsize=%#x" % s)
|
|
|
|
|
|
def va2off(va):
|
|
rva = va - image_base
|
|
for name, vaddr, vsize, rawptr, rawsize in sections:
|
|
if vaddr <= rva < vaddr + max(vsize, rawsize):
|
|
off = rva - vaddr + rawptr
|
|
if off < len(pe):
|
|
return off
|
|
return None
|
|
|
|
|
|
def cstr(va, limit=96):
|
|
off = va2off(va)
|
|
if off is None:
|
|
return None
|
|
end = pe.find(b"\0", off, off + limit)
|
|
if end < 0:
|
|
return None
|
|
try:
|
|
return pe[off:end].decode("ascii")
|
|
except UnicodeDecodeError:
|
|
return None
|
|
|
|
|
|
base = va2off(TABLE_VA)
|
|
print("\nroute table VA %#x -> file offset %s" % (TABLE_VA, hex(base) if base else None))
|
|
assert base, "table VA did not resolve"
|
|
|
|
print("\n%-4s %-34s %s" % ("#", "field A", "field B"))
|
|
rows = 0
|
|
for i in range(MAX_ROWS):
|
|
a_va, b_va = struct.unpack_from("<QQ", pe, base + i * 16)
|
|
a, b = cstr(a_va), cstr(b_va)
|
|
if a is None and b is None:
|
|
print("-- table ends after %d rows --" % rows)
|
|
break
|
|
print("%-4d %-34s %s" % (i, repr(a), repr(b)))
|
|
rows += 1
|