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
This commit is contained in:
funman300
2026-08-06 18:28:52 -07:00
parent 245c22161b
commit 31fc590b99
4 changed files with 156 additions and 13 deletions
+25 -5
View File
@@ -1157,8 +1157,13 @@ ROUTES = [
# tradePile MUST precede /trade ("/tradePile" contains the "/trade" prefix).
# /tradePile/counts (GetAuctionCount) MUST precede /tradePile: the latter's regex
# also matches the /counts path, and the two responses are different shapes.
(re.compile(G + r"/tradePile/counts"), lambda m, h: auction_counts_route(h)),
(re.compile(G + r"/tradePile"), lambda m, h: tradepile_route(h)),
# CASE-INSENSITIVE (live 2026-08-06): the FUT-hub Transfer List TILE polls the
# LOWERCASE `tradepile`/`tradepile/counts`, while the Transfer List SCREEN uses
# camelCase `tradePile`. Case-sensitive routes matched only the screen, so the
# tile fell through to /trade (which contains "trade") and got a shape the counts
# deser skips -> the tile read "Selling: 0" while a card was actively listed.
(re.compile(G + r"/tradePile/counts", re.I), lambda m, h: auction_counts_route(h)),
(re.compile(G + r"/tradePile", re.I), lambda m, h: tradepile_route(h)),
(re.compile(G + r"/trade"), lambda m, h: trade_route(h)),
(re.compile(G + r"/watchList"), lambda m, h: watchlist_route(h)),
(re.compile(G + r"/auctionhouse"), lambda m, h: auctionhouse_route(h)),
@@ -1252,13 +1257,28 @@ def _is_player(it):
def hub_data():
"""GET ut/%s/hub -- the FUT hub tile counters."""
"""GET ut/%s/hub -- the FUT hub tile counters.
The hub parser FUN_180139610 reads 18 atoms; two of them (auctionCount 0x33,
clubPlayers 0x90) we already serve. The FUT-hub 'TRANSFER LIST' tile
(items / Selling / Sold) is fed by a THIRD atom we were omitting: tradePile
(0x333), a nested object parsed by sub-deser 0x18013ead0. That sub-parser reads
count(0xbc), notification(0x1da), selling(0x2b8), sold(0x2c9) -- the same atom
scheme as GetAuctionCount (/tradePile/counts) -- each a SCALAR INT via the int
getter 0x1801c79d0 (5 int reads, one SKIP, an object field loop; no array, no
nested object => no type-desync surface). Confirmed 2026-08-06 straight from the
on-disk CardsDLL via objdump (scratchpad/hub_ladder.py).
LIVE SYMPTOM this fixes: a card was actively listed (auctionCount 1, Listed Items
showed it) yet the TRANSFER LIST tile read '0 items / Selling 0' -- the tile reads
hub.tradePile, not /tradePile/counts (which the tile never re-polls). All active
listings are 'selling'; none are 'sold'. count == selling == number of listings."""
if not HUBDATA:
return {}
players = len([i for i in STORE.items() if _is_player(i)])
auctions = len(STORE.listings())
log(" HUB: clubPlayers=%d auctionCount=%d" % (players, auctions))
return {"clubPlayers": players, "auctionCount": auctions}
log(" HUB: clubPlayers=%d auctionCount=%d selling=%d" % (players, auctions, auctions))
return {"clubPlayers": players, "auctionCount": auctions,
"tradePile": {"count": auctions, "selling": auctions, "sold": 0}}
# ---- club stats: the CLUB STATS panel, and probably the MY CLUB tile too ------