fifa17-recon: SOLVED "Send to Club" -- it was the response body all along

Live 2026-08-04 with FUT_MOVE_BODY=ack and FUT_PACK_AUTOCLUB=0. Bought a bronze pack,
opened it, chose Send to Club. The session SURVIVED, the five cards persisted into the
club pile, and there was no ut/delete/auth logout -- the logout that accompanied all
seven previous attempts.

  11:15:48 PUT /item
    req {"itemData":[{"id":100000125,"pile":"club","swap":0,"tradeId":0}, ... x5]}
    res {"itemData":[{"id":100000125,"pile":"club","success":true}, ... x5]}
  11:15:49 GET /user/credits      session alive
  11:15:51 GET /hub               no error dialog
  11:16:06 GET /club?year=2017... MY CLUB opened

PUT ut/%s/item never was an ack endpoint. It builds per-item VERDICT records, and the
completion handler raises EVENT_CARDS_MOVE_CARD_FAILURE when the vector is empty or
success != 1. Every body this project ever returned, {} included, told the client the
move had FAILED, and the client ended the FUT session because that is what that event
does. We were failing our own move.

Defaults flipped: FUT_MOVE_BODY empty -> ack, FUT_PACK_AUTOCLUB 1 -> 0. The autoclub
workaround is retired.

New intel, captured for the first time because the client had never got this far: the
request carries `swap` and `tradeId` beside id/pile. We ignore both and the move
succeeded, so neither is load-bearing for a pending-to-club move.

PROCESS, and this is the part worth keeping. The FIRST attempt at this test produced
no PUT /item at all: FUT_PACK_AUTOCLUB=1 had already emptied the pending pile at
purchase time, so the reveal screen had nothing to assign and the client never issued
the request. The workaround for the bug was hiding the bug. Before testing a fix,
check the configuration still lets the client make the call the fix is for.

Two self-inflicted incidents, both recorded in REBUILD_RESEARCH S17:
- Restarting the server to inject a flag WHILE FIFA was running produced the exact
  "error connecting to FIFA 17 Ultimate Team" dialog this project spent weeks chasing,
  from a plain connection refusal during the ~30s window. Restart only at the main
  menu, and check the log for ProtoHttp requests before blaming a response.
- pgrep -f matched the invoking shell twice, killing it before the restart, because
  the same command contained the literal script name in a later clause.

Docs updated: REBUILD_RESEARCH S17 (the solve), priority-2026-08 S2/S3.1/S6 (next task
is now the MY CLUB counter), PROJECT_REPORT 6a, HANDOFF 5a plus the stale "FutMoveCard
has no skip handler" claim in S3 and a new 5d for Seasons/Draft.

380 + 51 checks green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
This commit is contained in:
funman300
2026-08-04 11:20:03 -07:00
parent 18d864908e
commit 5b139864ee
5 changed files with 222 additions and 96 deletions
+42 -42
View File
@@ -617,53 +617,53 @@ def defs_route(h):
return 200, {"itemData": [item_def(i) for i in ids]}
# ---- pack reveal workaround ---------------------------------------------------
# THE REVEAL HAND-OFF IS UNSOLVED. Live 2026-08-04, five packs, three different
# response shapes for FutMoveCard (full card objects / +chemistry / dreamSquads-only):
# every time the client moved the cards, then POSTed ut/delete/auth ~1s later and
# dropped to the main menu. No crash dump; Blaze keeps pinging afterwards, so the
# game is alive and it is the FUT SESSION that ends. The cards always arrive
# server-side -- only the acknowledgement is rejected.
# ---- pack reveal: SOLVED 2026-08-04 -------------------------------------------
# The reveal hand-off worked live: five cards, Send to Club, session survived, cards
# persisted, no ut/delete/auth. See MOVE_BODY below for what actually fixed it.
#
# What is known: FutMoveCard's deserializer 0x180128600 contains NO skip handler
# (FUN_180135ff0 appears zero times, unique among FUT deserializers) and parses only
# itemData(0x16b) -> element -> dreamSquads(0xe9). Sending exactly that still failed,
# so the trigger is elsewhere and remains unidentified.
#
# WORKAROUND (default ON, FUT_PACK_AUTOCLUB=0 disables): deposit pack contents
# STRAIGHT into the club at open time and keep the pending pile empty, so the client
# is never offered a move to make and never sends the request that kills the session.
# Cost: the reveal screen shows no cards to assign. Benefit: packs are usable and the
# cards are in the club, which is the point of buying one. Turn this off when the
# real hand-off is understood.
PACK_AUTOCLUB = os.environ.get("FUT_PACK_AUTOCLUB", "1") == "1"
# The workaround this block used to describe (deposit pack contents straight into
# the club at open time, keep the pending pile empty) is now DEFAULT OFF. It was
# always a cost, not a fix: with the pending pile empty the client has nothing to
# assign, so the reveal screen shows no cards AND the move request is never sent.
# That second effect made the real bug untestable -- the first live attempt at the
# correct response shape produced no PUT /item at all because autoclub had already
# emptied the pile. Leave this off unless the move path regresses.
PACK_AUTOCLUB = os.environ.get("FUT_PACK_AUTOCLUB", "0") == "1"
# FUT_MOVE_BODY -- what PUT ut/%s/item answers. Made switchable so the shape can be
# bisected in one relaunch each instead of a code edit per attempt.
# ack -> {"itemData":[{"id":N,"pile":"club","success":true}, ...]}
# the CORRECT shape per the deserializer. UNTESTED LIVE.
# empty (default) -> {} KNOWN-BROKEN, see below
# full -> echo the moved card objects (known-broken)
# dreamsquads -> {"itemData":[{"dreamSquads":[]} x N]} (known-broken)
# FUT_MOVE_BODY -- what PUT ut/%s/item answers.
# ack (default) -> {"itemData":[{"id":N,"pile":"club","success":true}, ...]}
# LIVE-PROVEN 2026-08-04. Five cards sent to club, session
# survived, cards persisted, NO ut/delete/auth logout.
# empty -> {} known-broken
# full -> echo the moved card objects known-broken
# dreamsquads -> {"itemData":[{"dreamSquads":[]} x N]} known-broken
#
# `empty` REMAINS THE DEFAULT ONLY BECAUSE `ack` HAS NOT BEEN LIVE-TESTED.
# It is not a good default: 0x180128600 builds per-item VERDICT records and the
# completion handler raises EVENT_CARDS_MOVE_CARD_FAILURE when the record vector is
# empty, so {} fails unconditionally. Every rung above except `ack` is now known to
# report the move as failed. Flip the default to `ack` the moment one launch
# confirms it.
# WHY. 0x180128600 does not parse an acknowledgement, it builds per-item VERDICT
# records, and the completion handler raises EVENT_CARDS_MOVE_CARD_FAILURE when the
# record vector is EMPTY or when record+0x0c != 1. success(0x2fa) is initialised to
# '\0' per element. So {} and every echo shape reported the move as FAILED -- the
# session died because we told it to.
#
# The earlier note here argued the opposite -- that a bare {} was "demonstrably
# acceptable" because Quick Sell survives one. That inference was wrong: quick
# sell's callbacks check only the transport code and never read the body, so its
# tolerance says nothing about this endpoint. Retained as a caution: a sibling
# endpoint accepting a body is not evidence that this one will.
# The request the client actually sends (captured live, first time ever):
# {"itemData":[{"id":100000125,"pile":"club","swap":0,"tradeId":0}, ...]}
# so `swap` and `tradeId` accompany id/pile. We ignore both; the move succeeded
# without honouring them.
#
# Still true and still useful from that round: the netwatch recorded ZERO
# non-loopback connections during a failure, so "error connecting to FIFA 17
# Ultimate Team" is FIFA's generic FUT-session failure text and must not be read as
# a network event.
MOVE_BODY = os.environ.get("FUT_MOVE_BODY", "empty")
# HISTORY, kept because the wrong version of it cost seven attempts. This file used
# to claim 0x180128600 had NO skip handler and parsed only itemData -> dreamSquads.
# Both false: two skip-handler sites, seven atoms. The claim came from searching a
# TRUNCATED decompile (src[:4000] of 6193 chars). It implied "the body cannot be the
# problem", which sent the investigation after client-side state. It was the body.
# See REBUILD_RESEARCH.md S16.
#
# Also retracted: the argument that a bare {} was "demonstrably acceptable" because
# Quick Sell survives one. Quick sell's callbacks read only the transport code and
# never touch the body, so its tolerance said nothing about this endpoint.
#
# Still true from that round: the netwatch recorded ZERO non-loopback connections
# during a failure, so "error connecting to FIFA 17 Ultimate Team" is FIFA's generic
# FUT-session failure text and must never be read as a network event.
MOVE_BODY = os.environ.get("FUT_MOVE_BODY", "ack")
# FUT_STORE_GROUPS: send displayGroup(0xd9) in the pack catalogue. DEFAULT OFF --
# it FROZE the store screen live on 2026-08-04 (recursive nested array through the