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>
This commit is contained in:
funman300
2026-08-06 12:47:02 -07:00
parent e578443d73
commit a3fd51692f
5 changed files with 356 additions and 17 deletions
+46
View File
@@ -669,6 +669,52 @@ FUT_RS4_CONFIG = (
"IS_FIFAPOINT_PURCHASABLE", "IS_EASTORE_SERVICE_READY",
"COINS_PURCHASE_ENABLED", "POINTS_PURCHASE_ENABLED", "MONEY_PURCHASE_ENABLED",
)]
# FUT_TRADING: the transfer-market equivalent of the store block above.
#
# WHY THIS IS HERE AND NOT IN /settings. "Place on Transfer List" and "List on
# Transfer Market" are greyed out because the TO_TRADE_PILE predicate
# FUN_1801a7260 needs a service gate at vtable+0x270, which is
# `movzx eax, byte [rcx+0x1fd2e]; ret`. That byte is the tradingEnabled gate and it
# reads 0.
#
# Sending tradingEnabled through /settings does NOT move it, PROVEN live 2026-08-06:
# the arm is right (case 0x336 writes param_2[10]) and the applier is right
# (0x1fd2e = param_2[10] == 1), but the applier has NO caller Ghidra can see and is
# not reachable from the settings deserializer. The decisive measurement: we served
# maximumTradePileSize=77 and NO int gate field carries 77 (+0x1fd14=0, +0x1fd4c=0,
# +0x1fd54=480). Every gate byte is a constructor default. That also explains
# storeEnabled reading 1: a default, never our value.
#
# REFUTED 2026-08-06, KEPT ONLY AS A RECORD. THIS DOES NOT WORK. Do not turn it on
# expecting an effect, and do not reason from it.
#
# The reasoning above was wrong in two places and the flag is inert:
#
# 1. IS_TRADING_ENABLED IS AN OUTPUT NAME, NOT AN INPUT. FUN_18006cc60 is a
# PUBLISHER: at 0x18006ccc6 it does `call [rax+0x270]` (which reads 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 the
# whole of .text is that lea. There is no comparison against it anywhere, so a
# client-config key of that name cannot be read as an input by anything. The same
# is true of the IS_* store keys above, which means the store block may also be
# inert and its apparent success was never actually attributed.
# 2. The gate byte was briefly measured as 1 and that was over-claimed as a success.
# On a fresh session it reads 0, and a thorough re-measurement read 0 on the very
# pid where it had read 1. Either the first read was transient or something clears
# it after login. The only writer of 0x1fd2e is FUN_18011dc50 at 0x18011dc91.
#
# What IS now known, and supersedes the "/settings is dead" claim in the note above:
# FUN_18011dc50 is NOT unreachable. It is a VIRTUAL method at model vtable slot
# +0x988 (absolute pointer at 0x18021cc28), which is why a direct-call search found
# no callers. The real chain is
# settings response -> FUN_180174630 -> FUN_18013c6d0 (deser)
# -> completion callback FUN_180173e00 -> vt+0x988 / 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.
#
# Default OFF and it should stay off.
+ ([(k, "1") for k in ("tradingEnabled", "IS_TRADING_ENABLED")]
if os.environ.get("FUT_TRADING") else [])
# NOTE: do NOT advertise itemDbVersion/checkServerDbVersion here or in any
# response -- proven inert (wf_96b6c0c5): they are JSON field names that route
# to the value-SKIP handler 0x180135ff0, never compared. See docs/CARD_SYSTEM.md.