Files
OpenFUT/fifa17-recon/tools/ghidra_queries/objdump_atom_ladder.py
T
funman300 31fc590b99 fifa17-recon: the FUT-hub Transfer List tile counts, and the hub parser is NOT reflection
The Transfer List hub tile read "0 items / Selling 0" while a card was actively
listed. Enumerating the /hub parser FUN_180139610 straight from the on-disk
CardsDLL (objdump) refutes the old ENDPOINT_MAP claim that it uses C++ reflection
with "no atom ladder, nothing to enumerate": it has an ordinary running-sum atom
ladder reading 18 atoms. The tile is fed by hub.tradePile (0x333), a nested object
(sub-deser 0x18013ead0) reading count/selling/sold as scalar ints -- the same
scheme as GetAuctionCount, so serving it in the hub body is freeze-safe. The tile
never re-polls the standalone /tradePile/counts, which is why fixing that endpoint
alone did not move the tile.

Also: the hub tile polls LOWERCASE tradepile/counts while the Transfer List screen
uses camelCase tradePile; our case-sensitive routes matched only the screen, so the
tile's counts call fell through to /trade and got a shape the counts deser skips.
Made the tradePile routes case-insensitive.

And bake the proven transfer-market flags (FUT_TRADING/PILESIZES/TRADEABLE/
DISCARD_TABLE/DISCARD_SEND) into openfut-fut.sh so a plain `start` brings up the
working state instead of regressing trading to greyed-out.

- tools/utas_server.py: hub_data() serves tradePile:{count,selling,sold};
  tradePile routes now re.I
- tools/openfut-fut.sh: utas launched with the working flag set
- docs/ENDPOINT_MAP.md: full 18-atom hub map + tile map, correction of the
  reflection claim
- tools/ghidra_queries/objdump_atom_ladder.py: the objdump-based atom-ladder
  decoder used to derive the above

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lrx9to3pihN6Sm9sXgc8np
2026-08-06 18:28:52 -07:00

94 lines
3.2 KiB
Python

#!/usr/bin/env python3
"""Decode the running-sum atom ladders in /hub parser FUN_180139610 and name each
atom from docs/fut_atoms.tsv.
The dispatch is `sub ecx,d0 / sub ecx,d1 / .../ cmp ecx,dN`: the atom that each
branch handles is the CUMULATIVE sum of the deltas up to and including that step
(a jz after each sub tests atom==running_sum). Plus there are direct `cmp esi,imm`.
"""
import subprocess, re
DLL = "/tmp/fut/cardsdll.dll"
TSV = "/home/alex/Documents/OpenFUT/fifa17-recon/docs/fut_atoms.tsv"
FUNC, STOP = 0x180139610, 0x18013e600
atoms = {}
for line in open(TSV):
p = line.rstrip("\n").split("\t")
if len(p) >= 3:
try: atoms[int(p[1], 16)] = p[2]
except ValueError: pass
out = subprocess.check_output(
["objdump", "-d", "-M", "intel",
"--start-address=%#x" % FUNC, "--stop-address=%#x" % STOP, DLL], text=True)
# linear list of (addr, mnem, dest_reg, imm) for sub/cmp on 32-bit regs, stop at int3 pad
seq = []
int3 = 0
for ln in out.splitlines():
parts = ln.split("\t")
if len(parts) < 3:
continue
addr_s = parts[0].strip().rstrip(":")
try:
addr = int(addr_s, 16)
except ValueError:
continue
instr = parts[2].strip()
bits = instr.split(None, 1)
mnem = bits[0]
ops = bits[1].strip() if len(bits) > 1 else ""
if mnem == "int3":
int3 += 1
if int3 >= 4: break
continue
int3 = 0
mo = re.match(r"(e?[a-d]x|e?si|e?di|e?bp|r\d+d?),\s*(0x[0-9a-f]+)$", ops)
if mnem in ("sub", "cmp") and mo:
seq.append((addr, mnem, mo.group(1), int(mo.group(2), 16)))
# walk ladders: consecutive sub/cmp on the SAME register form one ladder; the running
# sum at each element is the atom that element dispatches. A `cmp` closes the ladder.
found = {} # atom -> (addr, kind)
i = 0
while i < len(seq):
addr, mnem, reg, imm = seq[i]
# a ladder starts on a sub
if mnem == "sub":
run = 0
j = i
while j < len(seq) and seq[j][2] == reg and seq[j][1] in ("sub", "cmp"):
run += seq[j][3]
found.setdefault(run, (seq[j][0], "ladder"))
if seq[j][1] == "cmp":
j += 1
break
j += 1
i = j
else:
# a lone cmp reg,imm on an atom-holding reg is a direct atom test
if 0 < imm <= 0x400:
found.setdefault(imm, (addr, "direct"))
i += 1
TOKENS = {0x1, 0x6, 0x7, 0x9, 0xa, 0xb, 0xc, 0xd} # SAX token enum, not atoms
print("Atoms dispatched by hub parser FUN_%#x:" % FUNC)
print("=" * 70)
for a in sorted(found):
if a in TOKENS:
continue
tag = " <-- TOKEN?" if a < 0x10 else ""
print(" %#06x %-28s (%s @ %#x)%s" %
(a, atoms.get(a, "?"), found[a][1], found[a][0], tag))
print("\nKnown tile counters for reference: 0x33=auctionCount, 0x90=clubPlayers")
print("\nName-based tile-count candidates:")
KEYS = ("sell","sold","trade","auction","pile","list","count","num","offer",
"won","outbid","target","watch","transfer","active","unassigned")
for a in sorted(found):
if a in TOKENS: continue
n = atoms.get(a, "").lower()
if any(k in n for k in KEYS):
print(" %#06x %s" % (a, atoms.get(a, "?")))