Files
OpenFUT/fifa17-recon/tools/route_table_dump.py
T
funman300 2e97ff1461 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.
2026-08-17 20:10:26 +00:00

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