diff --git a/OPENFUT_HANDOFF.md b/OPENFUT_HANDOFF.md index 186465b..20d9d4e 100644 --- a/OPENFUT_HANDOFF.md +++ b/OPENFUT_HANDOFF.md @@ -74,9 +74,12 @@ at `0x1801c7f1a`. This is the single most common way to break the game, and it h this project repeatedly. Arrays must be arrays; nested objects must be objects. **Unknown keys are usually skipped safely** — most deserializers route an unrecognised -atom to a value-skip handler (`FUN_180135ff0`), so extra fields are inert. **But not -always**: at least one deserializer (`FutMoveCard`, `0x180128600`) has *no* skip handler -at all, so any unexpected key leaves its value unconsumed and desyncs the reader. +atom to a value-skip handler (`FUN_180135ff0`), so extra fields are inert. This document +previously named `FutMoveCard` (`0x180128600`) as an exception with no skip handler at +all. **That was wrong**, and the retraction is in §5a: it has two skip-handler call sites +and parses seven atoms. No deserializer in this project is currently known to lack one. +Treat any future "this class has no skip handler" claim as unproven until the search is +shown to have covered the whole function. ### Working method @@ -97,7 +100,7 @@ omission is skip-safe, a wrong shape freezes the game. | Squad building — `PUT /squad/` fires, persists across relaunch | ✅ | | Squad roster ("MY SQUADS") | ✅ | | Transfer market — browse, bid, buy-now, sell, watchlist | ✅ | -| Packs — buy, cards land in club | ✅ (via a workaround, see §5) | +| Packs — buy, reveal, Send to Club | ✅ (the workaround is retired, see §5a) | | Quick sell — destroys cards, credits coins | ✅ | | Match loop — create/ready/play/destroy + coin rewards | ✅ implemented, **never played in-game** | | Online/EASFC status bar (no "servers unreachable") | ✅ when enabled | @@ -114,37 +117,34 @@ screens were broken by shipping "corrections" on by default. ## 5. Open problems -### 5a. "Send to Club" kills the FUT session — UNSOLVED +### 5a. "Send to Club" kills the FUT session — SOLVED 2026-08-04 -Opening a pack shows the cards correctly. Choosing **Send to Club** results in: +Opening a pack shows the cards correctly; choosing **Send to Club** used to produce +*"there has been an error connecting to FIFA 17 Ultimate Team"* and a logout, seven +attempts running. The cards always moved server-side; only the acknowledgement was +rejected. -> *"We are sorry but there has been an error connecting to FIFA 17 Ultimate Team. You -> will be returned to the FIFA 17 Main Menu."* +The cause was the response body. `PUT ut/%s/item` returns per-item **verdict** records, +not an acknowledgement, and the completion handler raises +`EVENT_CARDS_MOVE_CARD_FAILURE` when the record vector is empty or when `success != 1`. +Every body the project returned, `{}` included, reported the move as failed. The fix is +the real shape: -The client then POSTs `ut/delete/auth` (logout) about a second later. The cards **do** -move correctly server-side every time — only the acknowledgement is rejected. +```json +{"itemData":[{"id":100000125,"pile":"club","success":true}, ...]} +``` -**Eliminated by live test:** +Live: five cards to the club, session survived, cards persisted, no `ut/delete/auth`. +The autoclub workaround is retired. -| Hypothesis | Result | -|---|---| -| Missing `chemistry` field | Failed without it too | -| Extra keys + no skip handler in the deserializer | Real finding; sending *only* the parsed key still failed | -| The response body at all | **A bare `{}` also fails** | -| A network failure ("error connecting") | Zero non-loopback connections during a failure — that string is FIFA's *generic* session-error text | -| Missing per-call service URLs | 146 were genuinely missing and are now served; unchanged | -| The POW layer saturating HTTP | Same failure with POW fully disabled | -| The reveal screen's exit path | **"Quick Sell All" from the same screen works** | -| The club not being loaded | Loading MY CLUB first, then moving, still failed | +Two corrections this closed, both worth carrying forward: -**The key asymmetry:** quick sell (`POST ut/delete//item`) and move -(`PUT ut//item`) both receive an identical bare `{}` from the same server, with the -same headers and status. Quick sell succeeds; move kills the session. Whatever decides -this is **client-side state, not the wire**. - -**Workaround in place:** pack contents are deposited straight into the club at open time -and the pending pile is kept empty, so the client is never offered a move. Packs are -fully usable; the cost is that the reveal screen shows nothing to assign. +- The repo's claim that this deserializer had **no skip handler** and parsed only two + keys was false. It came from searching a truncated decompile. It implied the body + could not be at fault, which is what sent seven attempts after client-side state. + Never conclude an absence from a truncated or unverified-length extraction. +- The quick-sell asymmetry was not evidence of client state. Quick sell's callbacks + read only the transport status code and never touch the body. ### 5b. "MY CLUB" counter always reads 0 — UNSOLVED @@ -171,6 +171,24 @@ element parser**. Sending it populated **froze the store** (the type-desync busy it is behind a flag, default off. Doing it properly needs the group's own field set worked out rather than a self-referential copy of the pack. +### 5d. Seasons and Draft refuse — UNSOLVED, and not obviously server-side + +Selecting **single-player Seasons** raises *"There was a problem communicating with the +FIFA Ultimate Team servers"* while making **zero requests to any layer**. UTAS, Blaze and +POW logs show only pings and one census subscription across the whole failure window. No +response can be wrong because no request was made. POW is eliminated (same failure with it +enabled and disabled). + +**Online Draft** hangs the client rather than crashing it (process alive, no dump). The +one suspicious thing on the wire is `GET ut/%s/squad/mode/draft/state`, which our generic +`/squad` route answers with a full active-squad object: 23 slots, nested `itemData`, a +33-integer formation string. The real class wants `roundsInfo` plus a state enum, so this +is a textbook type-desync candidate and the timing matches. **Nothing has isolated it**; +it is a suspect, not a cause. + +Both matter beyond themselves, because they are the only two routes into a match, and the +`/match` request shape has therefore never been captured. + --- ## 6. Notable reverse-engineering findings @@ -230,16 +248,17 @@ out rather than a self-referential copy of the pack. ## 9. Where help would be most valuable -1. **The move-vs-quick-sell asymmetry (§5a).** Two sibling endpoints, identical responses, - one works. What client-side precondition could a "move item between piles" operation - have that a "discard item" operation does not — a loaded destination collection, a - known pile capacity, a valid target index? -2. **The MY CLUB counter (§5b).** Given the client demonstrably *has* the items and +1. **The MY CLUB counter (§5b).** Given the client demonstrably *has* the items and *renders* them, what else could a tab counter read from? Note that a sibling counter in the same bar works correctly. -3. **Whether either is fixable server-side at all**, or whether the honest answer is that - the deciding logic lives in the packed executable and only live instrumentation can - settle it. +2. **Seasons refusing with zero requests to any server (§5d).** The client raises a + "problem communicating with the FIFA Ultimate Team servers" without contacting + anything. Nothing on the wire can be wrong because nothing went on the wire. +3. **Whether the remaining problems are fixable server-side at all**, or whether the + deciding logic lives in the Denuvo-packed executable and only live instrumentation can + settle it. Note that this question was asked about `Send to Club` too, and there the + answer turned out to be a plain wire fix, so treat "it must be client-side" as a + hypothesis needing evidence rather than a fallback explanation. Useful framing: this project's failures have almost always come from proposing a fix before testing the assumption under it. Hypotheses that come with a cheap way to diff --git a/OPENFUT_PROJECT_REPORT.md b/OPENFUT_PROJECT_REPORT.md index ff83df4..15e441b 100644 --- a/OPENFUT_PROJECT_REPORT.md +++ b/OPENFUT_PROJECT_REPORT.md @@ -177,24 +177,40 @@ two working screens — once freezing the store outright. ## 6. Open problems -### 6a. "Send to Club" ends the FUT session +### 6a. "Send to Club" ends the FUT session — SOLVED 2026-08-04 -Opening a pack displays the cards correctly. Choosing **Send to Club** produces *"there -has been an error connecting to FIFA 17 Ultimate Team"* and a logout. The cards **do** -move correctly server-side; only the acknowledgement is rejected. +Opening a pack displayed the cards correctly, but choosing **Send to Club** produced +*"there has been an error connecting to FIFA 17 Ultimate Team"* and a logout, seven +attempts running. The cards always moved correctly server-side; only the +acknowledgement was rejected. -Eliminated by live test: the response body (a bare `{}` fails identically), a missing -field, unknown-key desync, a network failure (zero external connections during a -failure — that error string is FIFA's *generic* session error), missing service URLs, -the online layer, the reveal screen's exit path, and the club not being loaded. +**It was the response body all along.** `PUT ut/%s/item` 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 +`success != 1`. Every body this project returned, `{}` included, therefore told the +client the move had failed, and the client ended the FUT session because that is what +that event does. Serving the real shape fixed it in one launch: -**The asymmetry:** quick sell and move receive an *identical* bare `{}` from the same -server. Quick sell succeeds. Move ends the session. The deciding factor is client-side -state, not the wire. +```json +{"itemData":[{"id":100000125,"pile":"club","success":true}, ...]} +``` -**Workaround:** pack contents are deposited straight into the club at open time, so the -client is never offered a move. Packs are fully usable; the reveal screen just doesn't -offer assignment. +Live result: five cards sent to the club, session survived, cards persisted, no +`ut/delete/auth` logout. Both flags are now defaults and the autoclub workaround is +retired. + +**Why it took seven attempts,** which is the part worth keeping: the project had +recorded that this deserializer had *no skip handler* and parsed only two keys. That +was false, produced by searching a **truncated** decompile (the first 4,000 characters +of a 6,193-character function). It implied "the body cannot be the problem", which is +what redirected the investigation to client-side state. The quick-sell asymmetry that +seemed to confirm it has a mundane explanation: quick sell's callbacks read only the +transport status code and never touch the body, so its tolerance of `{}` said nothing +about this endpoint. + +The general lesson, now a standing rule: **never conclude an absence from a truncated +or unverified-length extraction**, and treat every negative claim in the endpoint docs +as weaker than the corresponding positive one. ### 6b. "MY CLUB" counter reads 0 diff --git a/fifa17-recon/docs/REBUILD_RESEARCH.md b/fifa17-recon/docs/REBUILD_RESEARCH.md index 5cb7029..1d12378 100644 --- a/fifa17-recon/docs/REBUILD_RESEARCH.md +++ b/fifa17-recon/docs/REBUILD_RESEARCH.md @@ -643,3 +643,74 @@ wire the whole time. **Status: necessary condition identified, sufficiency UNTESTED.** Staged behind `FUT_MOVE_BODY=ack`, default still `empty`. One launch settles it. + +--- + +## 17. SOLVED: "Send to Club" (2026-08-04, one launch) + +`FUT_MOVE_BODY=ack`, `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 the client carried on to the hub and then into MY CLUB. + +``` +11:15:42 POST /purchased/items pack bought, 8400 -> 8000 +11:15:43 GET /purchased/items 5 items in the pending pile +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:05 GET /club/stats/{staff,year,consumables} +11:16:06 GET /club?year=2017&type=player&count=11&... MY CLUB opened +``` + +**No `ut/delete/auth`.** Every one of the seven previous attempts logged that logout +within a second or two. This run has none. Profile on disk afterwards: 109 items, 86 +in the club pile, all five new ids present. + +### What this settles + +The failure was always the response body. The client was told, by every body this +project ever returned including a bare `{}`, that the move had **failed**, and it +ended the FUT session because that is what `EVENT_CARDS_MOVE_CARD_FAILURE` does. The +"deciding factor is client-side state, not the wire" premise recorded in §14c was +wrong, and §16 explains exactly which truncated decompile produced it. + +Both defaults are flipped in `utas_server.py`: `FUT_MOVE_BODY=ack`, and +`FUT_PACK_AUTOCLUB` now defaults **off**. + +### New intel: the request shape + +Captured for the first time (the client had never successfully reached this path): + +```json +{"itemData":[{"id":100000125,"pile":"club","swap":0,"tradeId":0}, ...]} +``` + +`swap` and `tradeId` accompany `id` and `pile`. We ignore both and the move +succeeded, so neither is load-bearing for a pending-pile-to-club move. `TODO/CONFIRM` +what `swap` means for a squad-slot exchange, where it plausibly is. + +### Process note, and it is not a small one + +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: it removed the only path that exercises the broken endpoint. + +The lesson generalises. A workaround that suppresses a request suppresses the evidence +too. Before testing a fix, check that the configuration still lets the client make the +call the fix is for. + +Two self-inflicted incidents in the same session, both worth recording: + +- The server was restarted to inject the flag **while FIFA was already running**, and + the user walked into the FUT hub during the roughly 30-second window with nothing on + :8099. That produced the exact "error connecting to FIFA 17 Ultimate Team" dialog + this project has spent weeks chasing, from a plain connection refusal. Restart only + when the client is at the main menu, and check the log for `ProtoHttp` requests + before attributing a failure to a response. +- `pgrep -f`/`pkill -f` matched the running shell twice, because the command that ran + the pattern also contained the literal script name in a later clause. It killed the + invoking shell before it reached the restart. Split the kill and the start into + separate commands, or obfuscate every occurrence. diff --git a/fifa17-recon/docs/priority-2026-08.md b/fifa17-recon/docs/priority-2026-08.md index bdd7d31..9b3682a 100644 --- a/fifa17-recon/docs/priority-2026-08.md +++ b/fifa17-recon/docs/priority-2026-08.md @@ -109,6 +109,20 @@ prove there is no second one. ## 2. The next task +> **RESOLVED 2026-08-04, the same day this was written.** The launch happened and it +> worked: five cards sent to the club, session survived, no logout, cards persisted. +> `FUT_MOVE_BODY=ack` and `FUT_PACK_AUTOCLUB=0` are now the defaults. Full account in +> `REBUILD_RESEARCH.md` §17. The section below is left as written, because the +> reasoning that put this first is the part worth reusing, not the outcome. +> +> One correction it earned: the pre-launch instruction to leave `FUT_PACK_AUTOCLUB=1` +> alone was wrong. Autoclub empties the pending pile at purchase time, so the reveal +> screen has nothing to assign and the client never sends the request. The first +> attempt produced no `PUT /item` at all. A workaround that suppresses a request +> suppresses the evidence for the bug it works around. +> +> **The next task is now §3.2, the MY CLUB counter.** + **One launch. Set `FUT_MOVE_BODY=ack`. Buy a pack, reveal it, press Send to Club.** Everything about this is cheap. One flag, one existing menu path, one binary outcome @@ -163,7 +177,13 @@ it, because an entry that cannot be disconfirmed does not belong on this list. ### 3.1 `Send to Club` under `FUT_MOVE_BODY=ack` -Covered in §2. Cost: one launch, one flag. Status: staged, unrun. +Covered in §2. Cost: one launch, one flag. **Status: run, and it worked.** Both flags +are now defaults. The measurement cost one pack and about four minutes. + +Worth noting against the "what could make this plan wrong" section below: the outcome +was the good one, but §7's warning still stands unchanged for everything else. This +result does not make the rest of the queue more likely to be right; it makes the +method more likely to be right. ### 3.2 The MY CLUB counter, `GET /club/stats/` @@ -417,8 +437,8 @@ corrected and must be fixed before they propagate into `ENDPOINT_MAP.md`. |---|---|---| | `FUT_MASSINFO` | `full` | live-proven; boot-critical | | `FUT_USERINFO` | `roster` | live-proven; `packs` and `full` are untested rungs | -| `FUT_PACK_AUTOCLUB` | `1` | the `Send to Club` workaround; revisit after §2 | -| `FUT_MOVE_BODY` | `empty` | `ack` is correct-by-decompiler but sufficiency is unproven | +| `FUT_PACK_AUTOCLUB` | `0` | the workaround is retired; it emptied the pending pile and suppressed the move request entirely | +| `FUT_MOVE_BODY` | `ack` | **live-proven 2026-08-04**; `empty`, `full` and `dreamsquads` all report the move as failed | | `FUT_MARKET` | `sample` | live-proven | | `FUT_MODES` | off | changes `/season`, whose array-root freeze risk cannot fire unless the client asks, and it has never asked | | `FUT_ACCOUNTINFO` | off | queued at §3.4 | diff --git a/fifa17-recon/tools/utas_server.py b/fifa17-recon/tools/utas_server.py index 88da4b1..5f3b1cc 100755 --- a/fifa17-recon/tools/utas_server.py +++ b/fifa17-recon/tools/utas_server.py @@ -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