Files
OpenFUT/fifa17-recon/tools/ghidra_queries/q_adv_mk_2.py
T
funman300 43557989f5 fifa17-recon: the transfer market works -- listed a card end to end, no freeze
The subsystem that was fully greyed-out this morning now lists a card on the transfer
market: price screen, Submit, "your item is now up for trade", TRANSFER LIST 0/100,
auctionCount 1, and STORE.listings() holds the auction. Every step verified at the
instruction level first, then confirmed live. Three fixes, all behind flags, all off by
default until this run proved them.

1. WE WERE BANNING OUR OWN TRADING. userInfo.feature (atom 0x11c) is a RESTRICTION map,
   not a grant; we sent feature={"trade":true}, which is a trade BAN. Verified in
   q_feature_trade.py: FUN_18013ec10 parses feature/trade into userInfo+0x17c, and at
   the massinfo END_OBJECT the client runs
     cmp byte [rsi+0x17c],0 / jz skip / mov dword [rsi+0x50],0
   feeding applier 0x18011dc91 -> IS_TRADING_ENABLED (model+0x1fd2e) = 0. It runs LAST
   and unconditionally, which is why the gate read 0 all day regardless of /settings or
   the Blaze config store. FUT_TRADING sends feature={} instead. Live: gate flipped
   0 -> 1 on UT re-entry (model rebuilt, pointer changed, byte read 1).

2. TRANSFER LIST CAPACITY 0/0. pileSizeClientData (massinfo atom 0x227, parser
   0x18013adb0) is the capacity, NOT the "MY CLUB counter" the old comment claimed.
   Verified in q_pilesize_keys.py: exactly two storing arms, key 2 -> model+0x1fd1c
   (TRADE_PILE_SIZE) and key 4 -> +0x1fd20 (watch list), every other key SKIP'd. The old
   code would have sprayed the 246 club count into the capacity. FUT_PILESIZES sends
   key 2 = 100, key 4 = 50. Live: capacity read 0 -> 100, header showed 0/100.

3. THE PRICE SCREEN FROZE THE CLIENT. GET marketdata/pricelimits was answered with an
   OBJECT {minPrice,maxPrice}; the deser 0x180163ee0 reads a BARE TOP-LEVEL ARRAY
   (root loop while tok != 0xd), so object-where-array desynced the SAX reader into the
   0x1801c7f1a busy loop (confirmed live: utime climbing 227 ticks/s, core pinned).
   Verified in q_pricelimits.py: element fields defId 0xcf, maxPrice 0x1c2, minPrice
   0x1ca, all scalar ints. marketdata_route now returns a bare array, one element per
   requested defId. Live: price screen opened and Submit succeeded.

Corrected along the way, all now in the code: two prior "trading root causes" from
earlier today were wrong (the Blaze IS_TRADING_ENABLED keys are output-only names, and
the applier is a virtual method at vtable+0x988, not unreachable). Those refutations are
recorded in blaze_responder_v3b.py and the doc.

Also lands the transfer-market recon doc (plan-2026-08-06-transfer-market.md) and the
market Ghidra query set.

Server-authoritative economy note: the 5% transfer fee and the price bands (currently a
150..15000 placeholder per defId) are not yet real; that is refinement, not a freeze.
The live-auction market SCREEN ("List on Transfer Market" browse) is a separate surface
still to do (P4 auction-counts route, P5 empty market bodies).

Live: 439 contract checks pass. Card listed and persisted, auctionCount 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:33:10 -07:00

85 lines
4.3 KiB
Python

"""ADVERSARIAL VERIFICATION BATCH 2.
Everything printed IN FULL with len(src). No truncation, no absence claimed from
a partial print.
H8 dim4 f5: auctionInfo record deser 0x18013e410 has exactly 12 atoms + tradeId
identity lookup via model vt+0xa00.
H9 dim4 f7: shared IS-list body 0x18013e7f0, credits -> model vt+0x5b8.
H10 dim4 f6: tradeState table walk FUN_180166bd0 (table 0x180229e40) and bidState
ladder FUN_180166380 -- two DIFFERENT dispatch forms, read separately.
H11 dim4 f8: FUN_1801844c0 status map, FUN_180165050 461 override.
H12 dim4 f9: FUN_1800377c0 IS_MAX_AUCTIONS + FUN_180163770 GetAuctionCount deser.
CONTROL for the publisher form: FUN_18000d550 TRADE_PILE_SIZE.
H13 dim4 f11: deser VAs for FutISWatchList / FutGetAuctionCount / FutISStart via
RS4 name -> abs64 ptr -> installed vtable -> slot +0x08, with FutISSearch and
FutGetTradePile as the CONTROL pair (must come back 0x180163420 / 0x180170810).
"""
import traceback, struct
def full(tag, va):
try:
s = dec(va)
print("\n----- %s %#x len=%d -----" % (tag, va, len(s)))
print(s)
except Exception:
traceback.print_exc()
try:
for tag, va in [("auctionInfo record deser", 0x18013e410),
("shared IS-list body", 0x18013e7f0),
("tradeState decoder", 0x180166bd0),
("bidState decoder", 0x180166380),
("status mapper", 0x1801844c0),
("ISOfferTrade 461 override", 0x180165050),
("IS_MAX_AUCTIONS publisher", 0x1800377c0),
("TRADE_PILE_SIZE publisher CONTROL", 0x18000d550),
("GetAuctionCount deser", 0x180163770),
("ISWatchList deser", 0x180166240),
("ISSearch deser CONTROL", 0x180163420),
("GetTradePile deser CONTROL", 0x180170810)]:
full(tag, va)
print("\n### tradeState table at 0x180229e40")
a = 0x180229e40
for i in range(10):
p = qword(a + i * 16); v = dword(a + i * 16 + 8)
if p == 0:
print(" [%d] NULL terminator, value=%d" % (i, v)); break
print(" [%d] %#x %r = %d" % (i, p, rd_str(p), v if v < 0x80000000 else v - (1 << 32)))
print("\n### H13 RS4 name -> installed vtable -> slot+0x08")
for nm, expect in [(b"RS4:FutISSearchServerResponse\x00", 0x180163420),
(b"RS4:FutGetTradePileServerResponse\x00", 0x180170810),
(b"RS4:FutISWatchListServerResponse\x00", None),
(b"RS4:FutGetAuctionCountServerResponse\x00", None),
(b"RS4:FutISStartServerResponse\x00", None),
(b"RS4:FutGetSuggestedPricingServerResponse\x00", None),
(b"RS4:FutRelistAllServerResponse\x00", None),
(b"RS4:FutISWatchTradeServerResponse\x00", None),
(b"RS4:FutISRemoveTradeServerResponse\x00", None),
(b"RS4:FutISRemoveWatchServerResponse\x00", None),
(b"RS4:FutISViewTradeServerResponse\x00", None),
(b"RS4:FutISOfferTradeServerResponse\x00", None)]:
locs = find_all(nm, blocks=(".rdata", ".data"))
print("\n %s -> %s" % (nm.decode().rstrip("\x00"), [hex(x) for x in locs]))
for L in locs:
xs = xrefs_to(L)
print(" xrefs: %s" % [(hex(a), t, f) for a, t, f, _ in xs])
for a, t, f, ent in xs:
if ent:
s = dec(ent)
# find the vtable it installs: look for PTR_ / &DAT_ assignment
import re
m = re.findall(r"(?:PTR_[A-Za-z_0-9]*_|DAT_|&)([0-9a-fA-F]{9})", s)
print(" fn %s @%#x len=%d installs %s" % (f, ent, len(s), set(m)))
for cand in set(m):
try:
vt = int(cand, 16)
if 0x180200000 <= vt < 0x180290000:
slot = qword(vt + 8)
print(" vtable %#x slot+0x08 = %#x (expect %s)"
% (vt, slot, hex(expect) if expect else "?"))
except Exception:
pass
except Exception:
traceback.print_exc()