31fc590b99
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
94 lines
3.2 KiB
Python
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, "?")))
|