docs+tools: dump the CardsDLL route table; narrow Q2 to one candidate by elimination
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.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user