288d9908215a6080d3647fe8e3cf244565865daf
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a7ac5a09fd |
fifa17-recon: club stats LIVE-PROVEN, default ON; S19's verdict was over-scoped
MY CLUB -> ENGLAND -> Premier League now reads 17. First non-zero number ever rendered
on that screen. Nothing froze, no other screen changed, 392 + 61 checks green with the
flag defaulted on and no env override.
S19 concluded "the MY CLUB counter is not server-fixable". That was too broadly scoped.
The nation and league drill-downs ARE server-fixable and are now fixed. S20 records the
corrected scope.
What made it work, from FUN_180043b90 case 3:
uVar7 = (**(param_2 + 0x18))(param_2, row, "LEAGUE_ID") THE UI ROW'S OWN ID
bronze/silver/gold = (+0x7f8)(store, uVar7, 2 / 3 / 4)
publish("PLAYERS_EMPLOYED", gold + silver + bronze) COMPUTED, never read
rare/kits/badges = (+0x7f8)(store, uVar7, 5 / 0x28 / 0x2d)
Still open and now correctly scoped: the hub tile's "0 TOTAL PLAYERS" and the MY CLUB
summary rows read the GLOBAL bucket via +0x800 in cases 1 and 5. We serve those rows.
The unchanged question is what SELECTS those cases, since the mode tag is copied from
the completed request and the client requests year, consumables, staff, country/<id>
and league/<id> but never club.
The method note, which is the durable part: two rounds of reasoning about this endpoint
produced two wrong bodies; twenty lines of the consumer produced the right one. Reading
the PARSER tells you what is accepted. Only reading the CONSUMER tells you what is used.
That question was answerable from the start and went unasked until live screenshots
forced it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
d2bbb4d378 |
fifa17-recon: S19 -- the MY CLUB counter is NOT server-fixable, with the mechanism
Two experiments, both negative, and the negative has a mechanism behind it rather than
being another failed guess.
FUT_CLUB_PAGE served 114 items to /club?...count=11 tile still 0 TOTAL PLAYERS
FUT_CLUBSTATS full stat set, players=114, all modes panel still Players 0
The bodies went out: four "CLUBSTATS: 11 stat rows (players=114)" responses in the log,
panel re-entered afterwards.
FUN_18012fbe0 store+0x78 = request+0xc4 the mode tag is copied from the
REQUEST that just completed
FUN_180043b90 switch (store+0x78)
case 1 +0x800(1)->PLAYERS_EMPLOYED, (0x1e)->BALLS_EARNED, (0x28)->KITS_AVAILABLE,
(0x14)->STADIA_OWNED, (10)->STAFF_EMPLOYED, (0x32)->TROPHIES_WON
case 6 CARDS_NO_TRAINING_*, CARDS_NO_CONTRACT_*, CARDS_NO_FITNESS_*
The MY CLUB summary is case 1, which needs mode 1 (club). The client requests staff,
year and consumables and NEVER club, so the tag settles at 6 and case 1 is never
selected. Our values are stored correctly (contextId 1 forces contextValue 0, the
global bucket the +0x800 getter reads) and case 1 reads exactly the six ids we set.
Nothing ever asks for them.
THE MODE IS CHOSEN CLIENT-SIDE FROM THE REQUEST URL. No response body can change it,
so there is no body that fixes this and generating more of them is wasted work.
Two independent corroborations rather than one story that merely fits:
- case 6 reads 0x3d CONTRACTS, 0x3e TRAINING, 0x40 FITNESS, exactly the three ids
FUN_18012fd40 cannot produce from any type string. The consumables view is
unsettable from this endpoint by construction.
- FUT_CLUB_PAGE eliminated the only other candidate: the tile is not a count of the
list we return.
Both flags stay implemented and default OFF. FUT_CLUBSTATS is correct against the
verified schema and would populate the moment a club-mode request occurred; deleting it
would throw away the schema work for no gain.
Recorded against myself: I argued from the matching labels (tile "TOTAL PLAYERS", panel
"Players", both zero while we served {}) that the two read the same store and one body
would fix both. The store IS shared. The SELECTION is not, and that is what decides it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
38b10e5ec5 |
fifa17-recon: S18d -- a testable hypothesis for the Seasons blocker
/leaderboards/options is the ONLY mode-related endpoint the real client has ever
requested, and we answer {} because FUT_MODES is off. Three of its seven occurrences
are followed within ~2 minutes by /user/accountinfo and a fresh /ut/auth, which is the
signature of hitting an error, returning to the main menu, and re-entering FUT.
Hypothesis: the client fetches mode options on entering the play area, caches them, and
later refuses Seasons from that cached EMPTY body without issuing another request. That
would explain the zero-requests-at-failure observation, which no response-shape theory
has been able to account for: the deciding fetch happened minutes earlier.
Stated as a hypothesis, not a finding. The correlation is real; the causation is not
established. Cheap to test: leaderboard_route already implements an options body behind
FUT_MODES=1.
Risk noted in advance: FUT_MODES=1 also enables /season, whose array-root shape is a
flagged freeze candidate. That risk cannot fire while the client never asks. If this
hypothesis is right, a populated options body is precisely what would make it ask for
the first time, so succeeding at step one arms step two.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
dd8dddd7af |
fifa17-recon: log archaeology -- /club is only ever the SEARCH form
REBUILD_RESEARCH S18, from running futlog.py over the full 3044-request history. The client has requested /club six times and EVERY ONE carried a query string (year/type/count/position/level/nation/league/team/sort). The bare path has never been requested. ENDPOINT_MAP says GET ut/%s/club is FutGetClubInfo, whose only recognised member is user(0x36c), and concludes our itemData body is skipped and the club list must therefore be empty. The club list is NOT empty; every card renders. So either the query form dispatches elsewhere or the row is wrong. TODO/CONFIRM. Relevant to the MY CLUB counter: we ignore the query string completely and return all 109 items to a request asking for count=11 with position and sort filters, and our response carries no result total. A paged search response is exactly where a tab counter would read its number from. Also recorded: the unmapped view is a standing detector for suffix endpoints the URL template table cannot show. It has caught four so far. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
e5bfe58fc8 |
fifa17-recon: futlog.py -- client-filtered log reader, and a correction it caught
Implements the standing requirement recorded in priority-2026-08 S5: the User-Agent filter is now the DEFAULT in the log tooling, not an option. The real client sends ProtoHttp; our own probes send curl/* or Python-urllib/*. Reading the log unfiltered gave this project a materially wrong picture of itself (/clubUser and /user/list had 93 and 180 hits, none from the game). The old futlog.py was a one-off with a hardcoded path and no notion of who made the request. Replaced with a real tool: timeline or summary, body/response display, path regex, time window, status filter, and an --unmapped view that lists the endpoints the client wants and we catch-all. --all and --probes exist for when you deliberately want our own traffic. Over the full 3044-request history: 486 requests came from the game. IT IMMEDIATELY CAUGHT ME OVERSTATING SOMETHING. Yesterday's commit called the PUT /item request shape "captured for the first time (the client had never successfully reached this path)". False. There are NINE client PUT /item requests in the log, eight of them during the failed attempts, every one carrying swap and tradeId: 08:45:11 08:51:13 08:55:21 08:58:19 09:10:52 09:15:26 09:22:31 09:37:12 | 11:15:48 The request was on the wire and in the log the whole time. What was new was reading it. Same class of error as the truncated decompile in S16: evidence already collected and not looked at. REBUILD_RESEARCH S17 corrected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
5b139864ee |
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
|
||
|
|
f488793b34 |
fifa17-recon: RETRACT the FutMoveCard "no skip handler" claim; stage the ack shape
RETRACTION. This repo claimed, in REBUILD_RESEARCH S14c and in utas_server's
item_route comment, that FutMoveCard 0x180128600 "HAS NO SKIP HANDLER
(FUN_180135ff0 appears zero times, unique among FUT deserializers)" and "parses
only itemData -> dreamSquads". Every part of that is false. Full decompile:
FUN_180135ff0 call sites : 2 (offsets 5006, 6080)
atoms parsed : 7 active dreamSquads id itemData pile reason success
Cause: the decompile was written out as src[:4000] and then searched. The function
is 6193 chars, so BOTH skip-handler call sites and four of the seven atoms lay past
the cut. An absence was reported from a truncated listing -- the same failure mode
as the Memory.getBytes bytearray scan that silently returned zero hits. Never
conclude an absence without asserting the searched region covers the function.
Cost: the false claim implied "any extra key desyncs this parser", which sent the
investigation after client-side state for seven attempts, and the derived premise
"the deciding factor is client-side state, not the wire" was wrong too.
VERIFIED SHAPE. PUT ut/%s/item is not an ack endpoint; it returns per-item VERDICT
records:
id(0x15c) INT 0x1801c79d0 -> record+0x00
pile(0x226) STRING 0x1801c7aa0 -> enum 0x180142650 (club=7 purchased=6 trade=5)
success(0x2fa) BOOL 0x1801c7620 -> record+0x0c
reason(0x279) STRING -> "Destination Full" = 0xf
dreamSquads(0xe9) INT array; else -> FUN_180135ff0 (skip)
The completion handler raises EVENT_CARDS_MOVE_CARD_FAILURE when the record vector
is EMPTY or record+0x0c != 1, and success is initialised to '\0' per element. So
every body ever returned reported the move as FAILED, {} included. Quick sell
survives an identical {} because its callbacks read only the transport code and
ignore the body -- that is the whole asymmetry, and it was on the wire after all.
STAGED, NOT DEFAULTED. FUT_MOVE_BODY=ack emits the correct shape; the default stays
`empty` because sufficiency is untested. One launch settles it.
The ack is answered BEFORE the `if moved:` gate: under FUT_PACK_AUTOCLUB=1 the
cards are already in the club when the reveal asks to move them, so move_items()
returns nothing and a moved-derived body would be zero-record -- failing in exactly
the configuration ack exists to fix. Caught in review before it ran. success is
asserted only for ids that were moved now or are already in the club; anything else
gets an honest success:false rather than an invented verdict.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
5d5198f5d1 |
fifa17-recon: match rewards, POW online layer, account backend, quick sell
Second session. FUT core loop, the EASFC/POW online layer, a central account
backend, and a lot of corrections. Everything risky is behind an env flag with
the default set to whatever was live-proven.
WORKING END TO END (live-verified this session):
* match loop -- POST/PUT/POST/DELETE ut/%s/match, rewards via FutDestroyMatch
(0x180121b60). Play a match, get coins, W/D/L updates.
* packs -- buy, cards land in the club, session survives (FUT_PACK_AUTOCLUB=1)
* quick sell -- POST ut/delete/%s/item was UNMAPPED and paid NOTHING; six cards
were destroyed for 0 coins. Now credits discardValue.
* POW/EASFC online -- the "EA FC servers unreachable" banner is powdll's layer,
a THIRD http api on :8094 nobody had served. Redirect needs no root: powdll
FUN_18005a460 reads FIFA_POW_URL from the same client-config store as
ROSTERUPDATE_URL. FUT_POW=1.
* account backend -- fut_account.py replaces 7 hardcoded copies of the persona
across 5 files; club/persona/online-profile editable via CLI.
CORRECTIONS TO ENDPOINT_MAP (all re-extracted from the deserializers):
* FutStoreGetPackTypes: id/packType/isPremium/quantity/saleType/purchaseLimit/
purchaseCount are NOT skipped no-ops -- all are parsed. extPrice inner objects
take externalPriceId(0x11a), not amount/currency.
* FutMoveCard 0x180128600 has NO skip handler (FUN_180135ff0 appears zero times,
unique among FUT deserializers) and parses only itemData -> dreamSquads.
* class -> deserializer resolution: the name literal is preceded by a 4-BYTE
HEADER and the factory LEA points at the header, so look up name_addr - 4.
Six attempts failed on this; now ghidra_env.class_deser(). Unlocked 11 SBC/
Draft schemas.
* live-only endpoints the request table never lists: ut/%s/squad/list,
ut/%s/user/club, ut/%s/club/stats/*, ut/%s/clientdata/<key>. The template
table is a floor, not a ceiling -- the log is the only ground truth.
* 163 RS4 call names exist; we served 17. All now served.
FIXED: club/stats/* was answering with the entire 28-item club inventory on every
poll (it fell through to the generic /club route).
UNSOLVED: the pack reveal's "Send to Club" (PUT ut/%s/item) kills the FUT session
whatever we answer -- {} included -- while its sibling quick-sell endpoint accepts
a bare {}. Seven hypotheses eliminated by live test, documented in
REBUILD_RESEARCH.md S14c so none get re-walked. FUT_PACK_AUTOCLUB routes around it.
Also unfixed: store tiles render "unknown" (displayGroup is parsed RECURSIVELY by
the same element parser; sending it FROZE the store, so FUT_STORE_GROUPS=1 is
default off).
Tests: test_fut_contract.py 380 (live, read-only) + test_match_rewards.py 51 (pure).
Note: fut_store.py carries some pre-existing uncommitted changes from before this
session (pack catalogue ids, pending-pile behaviour) that could not be separated
from this session's additions in the same file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|