Files
OpenFUT/fifa17-recon/tools/ghidra_queries/q_pilesize_1.py
T
funman300 a3fd51692f fifa17-recon: the trading gate is still shut, and two of yesterday's conclusions were wrong
Ships the club-item subtype correction and the tradeable plumbing, and records two
refutations of claims made earlier in the same session. Nothing here is a working fix
for trading; the honest state is that the gate is still closed and we now know more
about why.

REFUTED 1: "the Blaze client-config store opens the trading gate". It does not, and the
flag is inert. IS_TRADING_ENABLED is an OUTPUT NAME. FUN_18006cc60 is a publisher: at
0x18006ccc6 it calls [rax+0x270] to READ gate byte 0x1fd2e, then lea rdx,[
IS_TRADING_ENABLED] and hands the value out under that name. The only rip-relative
reference to the literal 0x1801fc118 in all of .text is that lea; there is no comparison
against it anywhere, so no client-config key of that name can be read as an input. That
also undermines the IS_* store keys shipped beside it: their apparent success was never
actually attributed to them.

REFUTED 2: "the gate byte flipped to 1". It reads 0. It was measured as 1 shortly after
CardsDLL mapped and that was over-claimed as a success; a thorough re-measurement read 0
on the SAME pid and model pointer, and a fresh session reads 0 with an unambiguous raw
dump (model+0x1fd18.. = 01000000 00000000 00000000 00000000 3c000000 01 01 00 01, the 00
being 0x1fd2e). Either the first read was transient or something clears it after login.
The only writer is FUN_18011dc50 at 0x18011dc91, so a 0 means something RAN and wrote it.

AND THE "/settings IS DEAD" CLAIM FALLS TOO. FUN_18011dc50 is not unreachable: it is a
VIRTUAL method at model vtable slot +0x988 (absolute pointer 0x18021cc28). A direct-call
search found no callers because Ghidra does not resolve virtual calls, which is the same
dispatch-form trap that has now produced seven wrong verdicts here. The real chain is
    settings response -> FUN_180174630 -> FUN_18013c6d0 (deser)
      -> completion callback FUN_180173e00 -> vt+0x988 and vt+0x998 -> gate bytes
and FUN_180173e00 bails before applying anything unless the int at response+0x1c is
zero. Which atom writes +0x1c is unknown and is the thing worth chasing.

The measurement behind that claim also had a gap: it checked +0x1fd14, +0x1fd4c and
+0x1fd54 for the maximumTradePileSize=77 probe but NOT +0x1fd1c, which is the actual
TRADE_PILE_SIZE (read via vt+0xa58 = FUN_18011bf30). So the probe never tested the field
it needed to. Serving 77 and reading +0x1fd1c is the clean falsifier and is still open.

Recovered and worth keeping: an authoritative slot-to-name table from the publisher.
  vt+0x270 IS_TRADING_ENABLED -> +0x1fd2e        vt+0x2b0 IS_FRIENDLY_SEASON_ENABLED -> +0x1fd3a
  vt+0x2b8 IS_TOURNAMENT_QUIT_ENABLED -> +0x1fd3b vt+0x2c0 IS_PROCESSING_STATE_ENABLED -> +0x1fd3c
  vt+0x2c8 IS_DRAFT_MODE_ENABLED -> +0x1fd3d      vt+0x2d8 IS_STORY_MODE_REWARD_ENABLED -> +0x1fd3f
  vt+0x2f0 IS_RETURNING_USER_REWARDS_SCREEN -> +0x1fd40  vt+0xa58 TRADE_PILE_SIZE -> +0x1fd1c
That also locates the red TRANSFER LIST 0/0: it is +0x1fd1c, currently 0.

WHAT IS ACTUALLY SHIPPED HERE, all default off:
  * FUT_TRADEABLE sends untradeable=false. Verified landing at item+0x49 (stored
    INVERTED by case 0x361) on a live club record. Applied on every READ path, not only
    in _item(), because the save holds 246 items minted before the flag existed and the
    club route serves them straight from the save. That gap was caught by reading the
    served JSON, not by unit-testing the factory.
  * FUT_TRADING adds tradingEnabled and IS_TRADING_ENABLED to the Blaze config. Kept
    only as a record of the refutation, with the reasoning inline so nobody retries it.
  * fut_clubitems FAMILIES subtypes corrected: kit 9, stadium 10, badge 11 (cardtype 7,
    not 9), ball 30, league logo 31. Every previous value sat in the 0x91..0x96 TROPHY
    block. probe_shelf's candidate set lacked 9, 10 and 11, so the probe route the docs
    preferred could never have answered this for three of five families.
  * Club kits and badges now carry teamid, reintroduced ALONE after the 2026-08-05 crash
    (which was never bisected; value is the established suspect and that response also
    carried 30 items across five wrong subtypes). itemType dropped: it was unobserved and
    never copied into the record.

Live: 439 contract checks, 414 card-family checks, market suite, all pass. The transfer
market still refuses with zero requests and the menu entries are still greyed.

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

94 lines
4.4 KiB
Python

"""TRANSFER LIST shows 0/0. What feeds the trade-pile capacity?
STATE. tradingEnabled (gate byte 0x1fd2e) is now 1, proven live, delivered via the Blaze
client-config store after /settings was proven incapable of reaching the applier. Every
card ships untradeable:false. Both documented gates of TO_TRADE_PILE (FUN_1801a7260) are
therefore satisfied, and yet "Place on Transfer List" and "List on Transfer Market" are
STILL greyed. The header reads TRANSFER LIST 0/0 in red, so the capacity is zero and that
is the obvious third condition.
We send maximumTradePileSize=77 through /settings. That cannot work: the applier
FUN_18011dc50 has no caller Ghidra can see, is unreachable from the settings deserializer,
and a live probe showed no int gate field carrying 77 (+0x1fd14=0, +0x1fd4c=0,
+0x1fd54=480). So the capacity must arrive some other way, or default to 0.
CANDIDATE LITERALS found in CardsDLL: TRADE_PILE_SIZE, GetMaxPileSize, NUM_MAX_AUCTIONS,
IS_MAX_AUCTIONS, pileSizeClientData, TradePileFull, maximumTradePileSize.
QUESTIONS
Q1 Find the reader of the trade-pile capacity. Start from GetMaxPileSize and
TradePileFull and walk back to the field they read. Which struct offset holds it?
Q2 Is that offset the SAME one the settings applier would write (i.e. one of the int
fields at 0x1fd14 / 0x1fd4c / 0x1fd54), or a different home entirely? If it is an
applier field then capacity is unreachable for the same reason tradingEnabled was,
and the Blaze store is the only remaining lever.
Q3 Is TRADE_PILE_SIZE a CLIENT-CONFIG key, read the same way IS_TRADING_ENABLED is?
That is the decisive question, because IS_TRADING_ENABLED via the Blaze store worked
on the first try and the same delivery path would fix this. Find its xrefs and the
lookup that consumes it. Do NOT assume: an UPPER_SNAKE literal could equally be a
localisation key or a telemetry tag.
Q4 pileSizeClientData smells like the ut/%s/clientdata route, which we already serve.
Check whether the capacity is read from a clientdata blob instead. If so that is a
UTAS-side fix and needs no Blaze restart, which makes it much cheaper to test.
CONTROL: IS_TRADING_ENABLED is known to work through the Blaze client-config path. Find
how IT is consumed, and use that as the template for judging whether TRADE_PILE_SIZE is
consumed the same way. A candidate that is NOT read by the same mechanism is not a
candidate, however plausible the name looks.
COVERAGE: print decompiles in full with lengths. Enumerate all four dispatch forms before
any absence claim: == , != , case labels, and sub/dec ladders.
"""
import traceback
LITERALS = [b"TRADE_PILE_SIZE\x00", b"GetMaxPileSize\x00", b"NUM_MAX_AUCTIONS\x00",
b"IS_MAX_AUCTIONS\x00", b"pileSizeClientData\x00", b"TradePileFull\x00",
b"maximumTradePileSize\x00", b"IS_TRADING_ENABLED\x00"]
def dump(va, title, limit=None):
try:
f = func(va)
src = dec(va)
print("\n" + "=" * 78)
print("%#x %s body %d / decompile %d chars%s"
% (va, title, f.getBody().getNumAddresses() if f else -1, len(src),
"" if limit is None else " (first %d shown)" % limit))
print("=" * 78)
print(src if limit is None else src[:limit])
except Exception:
print("!! failed %#x" % va)
traceback.print_exc()
try:
print("=" * 78)
print("LITERAL LOCATIONS AND XREFS")
print("=" * 78)
interesting = {}
for lit in LITERALS:
name = lit[:-1].decode()
hits = find_all(lit)
print("\n%-24s %d hit(s)" % (name, len(hits)))
for h in hits:
print(" at %#x : %r" % (h, rd_str(h, 40)))
xs = xrefs_to(h)
if not xs:
print(" no direct xref (may be reached via a table)")
for frm, typ, fn, ent in xs:
print(" xref %#x in %s (entry %#x)" % (frm, fn, ent))
if ent:
interesting.setdefault(name, set()).add(ent)
print("\n" + "=" * 78)
print("CONSUMERS, decompiled")
print("=" * 78)
# IS_TRADING_ENABLED first: it is the working control and the template.
for name in ("IS_TRADING_ENABLED", "TRADE_PILE_SIZE", "pileSizeClientData",
"GetMaxPileSize", "TradePileFull", "NUM_MAX_AUCTIONS"):
for ent in sorted(interesting.get(name, []))[:2]:
dump(ent, "consumer of %s" % name, limit=6000)
except Exception:
traceback.print_exc()