4e31fb98a2a1bf71917b10458d2db213947b5a8d
62 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
805d754dc8 |
fix(fifa17): isolate patched-client capability per session
Harden the empty-My-Packs capability binding so a verified FIFA process can never
enable clean/no-sentinel Store topology for another unverified process that merely
shares its source IP. The prototype keyed the decision by source IP alone; two FIFA
processes (concurrent, or a relaunch) share an IP, so an unpatched process could
inherit a patched one's clean-v1 mode and crash. Source IP is now auxiliary only.
- Authoritative key = the per-login UTAS session id (X-UT-SID). /ut/auth now mints
a fresh unique SID per login (was a shared constant) and opens a session record
keyed by that SID; the client echoes it on every later call incl.
/store/purchasegroup (live-confirmed). The legacy constant is still accepted by
the retired security-question gate only, never to grant clean-v1.
- Session state: _FIFA17_SESSIONS[sid] = {ip, persona, resolver, mode, created,
last_seen}. Store mode freezes at the first /store/purchasegroup of the session
and is immutable thereafter. Fail-closed: unknown SID, or a SID presented from a
different source IP than it was opened on, resolves to the sentinel.
- Launcher capability (out-of-band; cannot know the SID) is matched by (ip, persona)
as a SINGLE-USE, short-TTL pending, bound to exactly one session at whichever comes
first: its login (pending predates auth), the registration (session already live),
or its first store request. Ambiguous same-(ip,persona) concurrent registration is
ignored-late -> both sentinel (never a wrong clean).
- Session cleanup: activity-based TTL sweep (sessions 3600s idle, pendings 120s);
reaping only removes expired entries and never affects another live session.
- account_sync now clears only stale pending for the machine (pre-launch hygiene);
it no longer resets a per-IP mode (there is no per-IP mode any more).
Backend-only: the launcher registration payload (already carries personaId) is
unchanged. Additive; P2 sentinel remains the else-branch and the default.
Tests: matrix A-Q incl. same-IP concurrent (K), same-IP+persona relaunch (L),
same-IP failed-patch (M), late-registration-vs-frozen-sessions (N), TTL expiry (O),
duplicate/idempotent registration (P), and register-before-login pending (Q).
|
||
|
|
b25761ea31 |
feat(fifa17): negotiate clean empty My Packs mode
Backend side of the handshake: suppress the synthetic 65534 My-Packs sentinel
ONLY for a session whose client has registered a verified resolver-guard
capability. Additive; the P2 active-sentinel path is retained as the else-branch
and the universal default. Fail-closed everywhere.
- Per-client state keyed by source IP (client_address[0]; the only per-connection
discriminator in this single-account, stateless backend): _FIFA17_STORE[ip] =
{resolver, mode}; mode in {None, "sentinel", "clean-v1"}, guarded by a lock.
- New POST /openfut/fifa17/capability endpoint: accepts only
{"capability":"empty_mypacks_resolver","version":1,...}; unknown capability or
version => 400 and records nothing (=> sentinel).
- account_sync (the launcher's required per-launch call) resets the per-ip record
=> a new FIFA process starts unfrozen with no inherited capability.
- Store topology is frozen at the FIRST /store/purchasegroup per session:
clean-v1 iff a v1 capability is registered, else sentinel; immutable thereafter
(late capability logged + ignored this session; a disappeared capability does
not un-freeze a clean session). This enforces the SESSION-STABLE invariant.
- store_catalog zero-owned-packs branch: clean-v1 emits NO mypacks group (the
client guard routes category -1 to Browse); every other case emits the existing
active 65534 sentinel verbatim. PACK_CATALOG / pack 70 / normal packs / profile
untouched. FIFA-17 only; not lifted into game-independent Core.
- Tests: full matrix A-J incl. concurrency isolation (two IPs, no global leak) and
no cross-process capability leak.
Design: docs/plans/FIFA17_PATCHED_CLIENT_CAPABILITY.md.
|
||
|
|
f42279f869 |
fix(fifa17): keep empty My Packs group client-safe
When the account owns zero unopened packs, store_catalog() emits a synthetic `mypacks` group placeholder (id 65534, absent from PACK_CATALOG). Change its state from "inactive" to "active".
Root cause (bug 6c): FIFA 17's Store/Scaleform path resolves the `mypacks` category even with zero unopened packs (category chosen client-side via the movie's CATEGORY_ID -> screen+0x290; no server field gates it). CardsDLL FUN_1800147f0 then dereferences the resolved group with no null guard, so an absent group crashes the client (CardsDLL+0x14882, [NULL+0x48], minidump-confirmed). An inactive placeholder avoids the crash but makes the Store report the pack unavailable on entry and bounce to the Hub; an active placeholder lets the Store open normally.
65534 stays economy-safe: pack_by_id() returns None, so store_buy()/purchased_items() cannot open it or grant items/coins, and grant_unopened_pack() rejects it. Explicit selection is rejected client-side ("This pack is no longer available") and sends no backend request. This is a FIFA-17 client-compatibility shim (P2), not an EA-authentic representation, confined to the FIFA-17 backend (not OpenFUT Core). A clean zero-pack UX needs a client-side fix (docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md).
Adds regression tests (test_empty_mypacks.py): empty -> one active 65534 placeholder (absent from PACK_CATALOG); non-empty [70] -> no placeholder, genuine pack shown; economy safety; normal packs 1/5/6/7 untouched.
|
||
|
|
83539e33ec |
fifa17-recon: take running-backend versions of 8 runtime files (direction fix)
The earlier reconcile committed the local working-tree versions of these files, which are OLDER than the deployed backend. The running container (C) is byte-identical to docker/fifa17-python/tools (B) and is a strict superset: it adds profile_path_for/select_account/ensure_security_question (fut_store), safe_header_for_log/safe_request_path/security_question_route (utas_server), account_sync_route/_match_call/match_ready_body, plus POW balance fields and match lifecycle support, with zero unique local functions lost. Reconciled tree is now a strict superset of B with every shared file byte-identical; verified via md5 map (0 missing, 0 differing). |
||
|
|
8cba70dc90 |
fifa17-recon: reconcile authoritative tools with running backend (B)
- Add 8 files present in docker/fifa17-python/tools but missing from the top-level tree: fut_accounts.py + 7 test_*.py contracts (all committed in the server's docker tree; byte-identical to the running image). - Preserve newer responder work already matching the running container: utas_server.py (offlineSeason), lsx_responder_v2.py (OPENFUT_BIND), blaze_responder_v3b.py, autopatch.py, pow_server.py, fut_store.py, test_fut_contract.py, fifa17-hook-m1.sh. - Add 30 newer ghidra_queries (draft purchase/state, SBC 9-26, runtime registries). Local tree is now a strict superset of B with all shared files byte-identical. |
||
|
|
cc694774a3 | wip: checkpoint FIFA 17 SBC research for Windows migration | ||
|
|
31fc590b99 |
fifa17-recon: the FUT-hub Transfer List tile counts, and the hub parser is NOT reflection
The Transfer List hub tile read "0 items / Selling 0" while a card was actively
listed. Enumerating the /hub parser FUN_180139610 straight from the on-disk
CardsDLL (objdump) refutes the old ENDPOINT_MAP claim that it uses C++ reflection
with "no atom ladder, nothing to enumerate": it has an ordinary running-sum atom
ladder reading 18 atoms. The tile is fed by hub.tradePile (0x333), a nested object
(sub-deser 0x18013ead0) reading count/selling/sold as scalar ints -- the same
scheme as GetAuctionCount, so serving it in the hub body is freeze-safe. The tile
never re-polls the standalone /tradePile/counts, which is why fixing that endpoint
alone did not move the tile.
Also: the hub tile polls LOWERCASE tradepile/counts while the Transfer List screen
uses camelCase tradePile; our case-sensitive routes matched only the screen, so the
tile's counts call fell through to /trade and got a shape the counts deser skips.
Made the tradePile routes case-insensitive.
And bake the proven transfer-market flags (FUT_TRADING/PILESIZES/TRADEABLE/
DISCARD_TABLE/DISCARD_SEND) into openfut-fut.sh so a plain `start` brings up the
working state instead of regressing trading to greyed-out.
- tools/utas_server.py: hub_data() serves tradePile:{count,selling,sold};
tradePile routes now re.I
- tools/openfut-fut.sh: utas launched with the working flag set
- docs/ENDPOINT_MAP.md: full 18-atom hub map + tile map, correction of the
reflection claim
- tools/ghidra_queries/objdump_atom_ladder.py: the objdump-based atom-ladder
decoder used to derive the above
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lrx9to3pihN6Sm9sXgc8np
|
||
|
|
245c22161b |
fifa17-recon: correct tradePile/counts shape, and narrow the marketdata array fix
Two follow-ups on the working transfer market.
1. GET /tradePile/counts now returns the FutGetAuctionCount shape
({count, maxAuctionsAllowed, offered, selling, sold}, all scalar ints, atoms
0xbc/0x1bf/0x1e5/0x2b8/0x2c9) via a dedicated route ordered before /tradePile.
Previously it fell through to tradepile_route and got the auction-LIST body, which
the counts deser skips, leaving every tally at its constructor default. Survivable
but wrong; the doc flags the loaded byte at +0x28 as gating a completion-handler
branch. selling reflects real STORE.listings().
2. Narrowed the marketdata bare-array fix to /pricelimits only. The client sends TWO
marketdata requests: /marketdata/pricelimits (GetSuggestedPricing, a bare array,
the thing that froze) and plain /marketdata?defId=N (price comparison, an OBJECT).
The prior commit returned the array for both, which the contract suite caught
(test_market_bodies: 'list' has no attribute get) -- plain /marketdata wants
{minPrice,maxPrice} and was never the freeze. Returning the array for it would be
the same desync in reverse. Now: pricelimits -> array, plain marketdata -> object.
The contract suite catching my over-broadened fix before it reached the game is the
suite doing its job. 439 contract checks pass, market unit suite passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
43557989f5 |
fifa17-recon: the transfer market works -- listed a card end to end, no freeze
The subsystem that was fully greyed-out this morning now lists a card on the transfer
market: price screen, Submit, "your item is now up for trade", TRANSFER LIST 0/100,
auctionCount 1, and STORE.listings() holds the auction. Every step verified at the
instruction level first, then confirmed live. Three fixes, all behind flags, all off by
default until this run proved them.
1. WE WERE BANNING OUR OWN TRADING. userInfo.feature (atom 0x11c) is a RESTRICTION map,
not a grant; we sent feature={"trade":true}, which is a trade BAN. Verified in
q_feature_trade.py: FUN_18013ec10 parses feature/trade into userInfo+0x17c, and at
the massinfo END_OBJECT the client runs
cmp byte [rsi+0x17c],0 / jz skip / mov dword [rsi+0x50],0
feeding applier 0x18011dc91 -> IS_TRADING_ENABLED (model+0x1fd2e) = 0. It runs LAST
and unconditionally, which is why the gate read 0 all day regardless of /settings or
the Blaze config store. FUT_TRADING sends feature={} instead. Live: gate flipped
0 -> 1 on UT re-entry (model rebuilt, pointer changed, byte read 1).
2. TRANSFER LIST CAPACITY 0/0. pileSizeClientData (massinfo atom 0x227, parser
0x18013adb0) is the capacity, NOT the "MY CLUB counter" the old comment claimed.
Verified in q_pilesize_keys.py: exactly two storing arms, key 2 -> model+0x1fd1c
(TRADE_PILE_SIZE) and key 4 -> +0x1fd20 (watch list), every other key SKIP'd. The old
code would have sprayed the 246 club count into the capacity. FUT_PILESIZES sends
key 2 = 100, key 4 = 50. Live: capacity read 0 -> 100, header showed 0/100.
3. THE PRICE SCREEN FROZE THE CLIENT. GET marketdata/pricelimits was answered with an
OBJECT {minPrice,maxPrice}; the deser 0x180163ee0 reads a BARE TOP-LEVEL ARRAY
(root loop while tok != 0xd), so object-where-array desynced the SAX reader into the
0x1801c7f1a busy loop (confirmed live: utime climbing 227 ticks/s, core pinned).
Verified in q_pricelimits.py: element fields defId 0xcf, maxPrice 0x1c2, minPrice
0x1ca, all scalar ints. marketdata_route now returns a bare array, one element per
requested defId. Live: price screen opened and Submit succeeded.
Corrected along the way, all now in the code: two prior "trading root causes" from
earlier today were wrong (the Blaze IS_TRADING_ENABLED keys are output-only names, and
the applier is a virtual method at vtable+0x988, not unreachable). Those refutations are
recorded in blaze_responder_v3b.py and the doc.
Also lands the transfer-market recon doc (plan-2026-08-06-transfer-market.md) and the
market Ghidra query set.
Server-authoritative economy note: the 5% transfer fee and the price bands (currently a
150..15000 placeholder per defId) are not yet real; that is refinement, not a freeze.
The live-auction market SCREEN ("List on Transfer Market" browse) is a separate surface
still to do (P4 auction-counts route, P5 empty market bodies).
Live: 439 contract checks pass. Card listed and persisted, auctionCount 1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
21a81ad63c |
fifa17-recon: the real quick-sell table, and the grouping bug is not in our layer
Multi-agent pass over the store subsystem, 11 agents, findings run through three
adversarial verifiers. Full writeup in docs/plan-2026-08-05-store-subsystem.md.
THE REAL DISCARD TABLE IS RECOVERED. quick_sell() paid an invented rating tier
(600/300/150/50) that was wrong for every single card. The real table is
fcc_discardcoins in the client's own game DB, 141 rows keyed (cardtype, level, rare),
read out of the running client and verified 22/22 against live items:
value = round_half_up(rating * price / 100)
level = 3 if rating >= 75, 2 if 65..74, else 1 (0x180141e8a..0x180141ea3,
derived from rating, NOT a wire field)
cardtype = FUN_1800d8330(cardsubtypeid), decoded from its jump table and checked
across every subtype 0..599 with zero disagreements
A 94-rated gold rare is 752, not 600. A 76 rare is 608, not 150. A 55 bronze is 17,
not 50.
This also closes a disagreement nobody had noticed: the CLIENT already computes and
displays the correct value locally whenever our discardValue (atom 0xd7) is 0 or
absent. FUN_18013fe00 stores our value at item +0x38 and the guard at 0x180141025
skips the local computation when it is non-zero. So the screen has been showing the
real number while the server paid a made-up one, on every quick sell ever made.
Verified beyond what the report claimed, because a missing table row pays ZERO and
that would be a regression the old flat tier could not produce: across all 236 items
in the live profile, 230 map to cardtype 1 and 6 to cardtype 6, and NOT ONE would pay
0 coins. Table reproduces at 141 rows and the worked example lands exactly.
ZERO WIRE CHANGE, FUT_DISCARD_TABLE default off. Nothing new is sent; only the coin
figure the server credits moves. This is the patch worth defaulting on after one
in-game check, which is simply quick-selling a card and seeing the coins paid match
the value the card was already displaying.
THE GROUPING BUG IS NOT IN CARDSDLL, and the fix ranked first would have wasted a
launch. Live in the running client all three display groups own exactly the right
pack, there is exactly one copy of each pack record in 4 GiB, and nothing we send is
mis-parsed. The parsed model is correct and the Scaleform layer picks the wrong pack
when turning a tile click into a category id. displayGroupAssetId is served as 1/5/6
while the screen's category field reads 3, and group tiles carry a hardcoded
CATEGORY_ID of 0. Confirmed by direct read: ordinal 3, assetId 6, i.e. Premium, while
the last click was Gold.
The heap map that made this possible, all scoped to one pid: display-group vector
control block, 3 elements of 0x108; group record fields at +0x00 sortPriority,
+0x04 displayGroupAssetId, +0x40 a one-element pack vector; inner pack record 0x1a8
with packType at +0x38, ids at +0x70/+0xac, price at +0xa0, quantities at +0xc0..+0xd0.
extPrice SHOULD BE DELETED, not corrected. Both sub-parsers read only
externalPriceId; amount and currency are discarded. Sending the key at all creates an
"mtx" currency row that switches on a real-money price line the client can never fill
offline, which is the literal "or %1s" on every tile.
A WORRY NOBODY HAD RAISED, and I confirmed it from our own logs: the client has sent
packId 6 on every purchase it has ever made, four for four tonight and six for six
across history. We have never observed a successful buy of anything but Premium Gold.
Also settled: FUT_STORE_DISPLAYGROUP=0 is the right resting state, argued from
mechanism rather than from history; FUT_USERINFO=packs stays off because the
unopened-pack counter is client-mutable and the flag ladder silently drops squadList;
POST /user is a latent hard freeze that has never fired because the client never
issues that POST.
Honest coverage: the ActionScript layer is unread by everyone and every remaining
store mystery lives there.
Live: 439 contract checks pass, market suite passes, both flags off.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3f3d5704a7 |
fifa17-recon: quick sell has never been reachable, and finalFunds is the rendered price
Live run, 2026-08-05 evening. Three results, one of them a route that has been dead for
the whole life of the project.
QUICK SELL WAS NEVER SERVED. Captured on the wire:
DELETE /ut/game/fifa17/item/100000240 (single card, id in the URL, no body)
ENDPOINT_MAP documented the path as `ut/delete/game/%s/item`, ROUTES was built from the
doc, and the regex therefore never matched a real quick sell. Every quick sell fell
through to the catch-all, so quick_sell_route() and STORE.quick_sell() behind it had
never once been called.
The empty response was not a harmless no-op. This body is BALANCE-BEARING: the client
takes its coin total from it, so with totalCredits absent it rendered an uninitialised
value. A real session showed 1,133,686,384 coins against a true balance of 9,889,600.
Display artifact only, corrected by the next GET /user/credits, and the save was never
touched, but it is the reason a stub is not acceptable here.
quick_sell_url_route() serves the real form and returns the corrected shape,
{"items":[{"id":N}],"totalCredits":N}. DEFAULT ON, which the house rule now permits:
two quick sells fired through it in one session, each credited 150 and removed the
card, and the coin arithmetic reconciled exactly against the 15,000 pack purchases
either side of them. An unknown id returns {"items": []} rather than claiming a sale we
cannot account for.
Still UNKNOWN and deliberately not chased: whether the client ASSIGNS totalCredits as
the new balance or ADDS it as a delta. We send the new balance. The discriminating
window is about five seconds wide, because the client refetches GET /user/credits
straight afterwards, so either reading self-corrects and the practical impact is a brief
wrong number. It mattered only while we answered {}, because that garbage persisted.
The credit amount is STORE.quick_sell()'s invented rating tier, not FUT's real discard
table, which remains unknown.
finalFunds IS THE RENDERED COIN PRICE. Served funds=15000 / finalFunds=4321 on one pack
and the tile read 4,321. funds is not displayed. ENDPOINT_MAP updated to CONFIRMED LIVE
with the method recorded. FUT_PRICE_PROBE, the flag that produced it, stays default off
and is disarmed: it puts a price on a tile that the buy path does not charge.
TWO UNPLANNED FINDINGS, both recorded for the next round rather than fixed here:
* The store grouping is broken and it is NOT cosmetic. All three packs collapse into
one display group, and the Bronze, Gold and Premium group tiles all drill into the
same single Premium Gold pack, so TWO OF THREE PACKS CANNOT BE BOUGHT. We send
displayGroup {"value": name} but never displayGroupAssetId (0xda), so everything
lands in group 0. The docs had this parked as a cosmetic "tiles read unknown"
issue; it is an availability bug.
* The FIFA Points price renders as the literal "or %1s", an unsubstituted printf
placeholder. extPrice.finalPrice is served as {"amount":N,"currency":"mtx"} and
"mtx" is evidently not a currency token the client resolves. Cosmetic.
Corrected in passing: packContentInfo DOES reach the tile (11 ITEMS / 11 GOLD /
11 RARES against exactly what we serve). An earlier screen showing zeros was the
display-GROUP level, which carries no content info. D3 was right.
Live: 439 contract checks pass, both probe flags off, store prices back to honest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
89da7b7609 |
fifa17-recon: /hub refutes yesterday's envelope conclusion, and two ENDPOINT_MAP freezes
Three things: the envelope rule was wrong and is corrected, /hub is settled, and two
documented response shapes that would freeze the client are fixed.
THE CORRECTION. The previous commit concluded that a three-token root consumes `{`, the
first field name and that field's value without dispatching them, so the first key of a
flat body was silently eaten, and that `login` had therefore never been delivered on
POST /user. That is WRONG and is withdrawn, along with the claim that the key order of
the auth dict is load-bearing.
The first call to FUN_1801c7f10 returns token 7 and consumes NO input. It is a
once-only start-of-document token, guarded by the flag at parser+0xda together with the
zero character counter at parser+0x30. So the three tokens are BOF, `{`, and the FIRST
FIELD NAME, and the key loop dispatches from that first key onward. The `== 10` test on
the third token is not an envelope check, it is the empty-object early-out: for `{}` the
third token is END_OBJECT and the root exits with its constructor defaults intact, which
is why answering `{}` has always been safe.
Corrected enum: 7=BOF 9=START_OBJECT 10=END_OBJECT 11=FIELD_NAME 12=START_ARRAY
13=END_ARRAY. The enum itself was right before; the inference from it was not.
HOW IT WAS CAUGHT, which is the part worth keeping. Not by more decompiling. /hub is
served flat and the wrong model predicted its first key would be discarded, so the
prediction was checked against the client's own memory: clubPlayers read back as 205,
the value the server sent, at model+0x1fd70+0x3c with the slide proven against the FNV
prologue first. One live read refuted a chain of otherwise sound static reasoning in
about a minute. tools/hub_counter_probe.py keeps it repeatable.
Consequence worth flagging: a wrapper is not just unnecessary for these roots, it would
be harmful, since a wrapper key hashes to an atom with no arm and the whole object is
skipped. That makes the createPackResponse envelope DOUBTFUL rather than confirmed.
Atom 0xbe has no arm in FUN_180162880. There is no live evidence either way because
nothing has ever parsed that body, so the buy path is left exactly as it is.
TWO ERRORS OF MINE ON THE WAY, both recorded in the doc because both are cheap to
repeat. I searched for RS4:FutGetHubServerResponse, found nothing and reported that no
hub class existed; the class is FutGetHubDataServerResponse (literal 0x18022ce40,
vtable 0x18022cd48, deser 0x1801738b0, control FutSquadSave -> 0x180171a60 matched in
the same run). Then I scanned 152 deserializers for clubPlayers, got zero hits and a
passing control, because the guard is `!= 0x90` and my pattern only matched `== 0x`.
The control passed only because auctionCount happens to use `==`. A control that does
not exercise the same code shape as the target is not a control. The comment already at
utas_server.py:1076 had the hub chain right the whole time.
ENDPOINT_MAP corrections, both freeze-risky as written, neither affecting what we serve
today:
* duplicateItemIdList is an ARRAY OF OBJECTS (element deser 0x180138e10), not the int
list at :1095. Bare ints where the element parser expects objects is a tokenizer
desync, i.e. a hard freeze at 0x1801c7f1a. Control that this is not a misread:
dreamSquads 0xe9 in FutMoveCard genuinely is a bare int array.
* FutDiscardCardServerResponse is {"items":[{"id":N}],"totalCredits":N}. There is no
top-level id.
No behaviour change. utas_server.py is comment-only. 439 contract checks pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1605e6effd |
fifa17-recon: the envelope rule, and the one key of the auth body that is silently eaten
The open question was whether a response deserializer DESCENDS a wrapper or PROBES for
one. Three roots spend an identical three tokenizer calls before dispatch, yet we serve
some bodies wrapped and some flat, and not all of those could be right.
TOKEN ENUM, decoded from the class table at DAT_18023dd40 and the switch in the
classifier FUN_1801c67a0 (the push/pop arms key off container state 2 = object,
3 = array):
9 START_OBJECT case 0x64, pushes state 2
10 END_OBJECT case 0x65, pops state 2
11 FIELD_NAME confirmed independently: FUN_18013bd40 tests +0xd0 == 0xb then
atom-hashes the string at +0xf8
12 START_ARRAY case 0x66, pushes state 3
13 END_ARRAY case 0x67, pops state 3
1 error the caseD_78 sink
So the three tokens are `{`, the first FIELD_NAME, and the token opening that field's
value. The envelope is structurally required and its name is NEVER hashed, which is why
FutCreatePack's ladder has no arm for createPackResponse (0xbe) and does not need one.
Coverage for that absence: the ladder has exactly four arms (0xec, 0x16e, 0x1dd, 0x264)
and 0xbe does not occur anywhere in the full 4702-char decompile, printed in full.
The competing reading rested on a factual error. It claimed the /purchased root spends
the same three tokens. FUN_180124ee0 spends TWO and hands off to FUN_18013bd40, which
spends the third. Same total, split across two functions. /purchased never was a
counterexample.
THE BUG THIS FOUND IS NOT THE ONE THAT WAS PREDICTED. The doc expected starterPack,
squad and userData to be swallowed on POST /user. They are not. FutCreateUser
(0x18014cc60) has ladder arms for exactly the five keys we send, and four of them
dispatch correctly at the outer level. The one that does not is `login`: its name is
eaten as the anonymous envelope and its value as the third token. It has an arm, so the
client wants it, and it has never once been delivered.
The second-order consequence matters more than the first. The key order of that dict is
load-bearing and nothing said so. Put userData first and the client loses the entire
user record, silently, with no error and no log line. That warning now sits in the code
next to the dict, which is the only place someone about to reorder it would look.
No behaviour change here. The utas_server.py edit is a comment. 439 contract checks
still pass. The probable proper fix, wrapping all five keys one level down inside a
single envelope key, is a hypothesis with a mechanism rather than a proven fix, and it
touches the login path, so it is not made here and would go behind a flag defaulting
off.
Writeup is section 2 of docs/plan-2026-08-05-pack-opening.md, added by the previous
commit. Opened by this and still UNKNOWN: GET /hub is answered with a flat two-key
body, which under this rule a three-token root would silently truncate, but there is no
FutGetHubServerResponse class and neither atom has a code xref, so /hub may not go
through a generated root at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
afdbb364ca |
fifa17-recon: pack opening reversed end to end, and there is no pack-inventory endpoint
A twelve-agent pass over the parts of pack opening we did not understand, run against
the live client (CardsDLL slide proven, not assumed) plus static CardsDLL. Findings
below survived an adversarial verification round that corrected several of them; where
a verifier and a finder disagreed, the verifier won.
THE HEADLINE IS A NEGATIVE, and it deletes work rather than creating it. There is no
pack-inventory endpoint in FIFA 17 and there never was. Proven three independent ways:
the 48-entry UTAS route template array at 0x18021df80, a regex for "ut/" over the whole
PE, and the 125-row client action table at 0x1802caa20, which is the complete set of
requests the client can originate. "Serve the pack inventory" comes off the backlog.
The unclaimed-pack tile and My Packs are two fields on responses we already build.
Corrections to ENDPOINT_MAP.md, both freeze-risky as written:
* duplicateItemIdList is an array of OBJECTS (element parser 0x180138e10: itemId
0x16d, duplicateItemId 0xeb, itemLoans 0x16f, duplicateItemLoans 0xed), not the
int list documented at :1095 and :218. Control that this is not a misread:
dreamSquads 0xe9 in FutMoveCard genuinely is a bare int array and parses with no
inner object loop. We serve [], so this is a docs bug today and a live freeze the
moment somebody implements it from the map as written.
* FutDiscardCardServerResponse is {"items":[{"id":N}],"totalCredits":N}. There is no
top-level id. :968-971 is wrong twice over.
packContentInfo is DECORATIVE. It is read only into a store-tile view model, and
nothing compares the declared counts against the delivered itemList, so open_pack()
does not have to honour the distribution.
The reveal is entirely CLIENT-SIDE. Walkout, tiering, colours and ordering are
arithmetic over fields we already send. Genuine outstanding server work reduces to
three items: duplicates, quick-sell credit, unopenedPacks.
Perishable intel captured: the real FIFA 17 retail pack catalogue, 41 SKUs with Origin
offer ids, recovered from the client heap as a parsed copy of data/store/storecfg.xml.
It is in no file on disk, only in a running process.
futmem/ is a standalone read-only Rust crate for this kind of work (maps, find,
strings, read). Read-only by construction: it opens /proc/<pid>/mem with File::open
and there is no code path in it that can write to another process, because a live game
session depends on that. Its own [workspace] table keeps it out of the parent
workspace. Chunked scanning overlaps by pattern_len-1 so a match spanning a chunk
boundary is still found.
utas_server.py gains FUT_PORT/FUT_LOG so a throwaway instance can be started without
bouncing the one the live client is using. Defaults unchanged (8099, /tmp/utas_server.log).
Noted for the record: this edit came from a research agent that had been told not to
touch server code. It is benign and useful, but it was out of scope.
Not committed: the doc proposes ENDPOINT_MAP.md changes as pasteable text rather than
applying them, and every proposed server change defaults off per the house rule.
Nothing in this commit changes a response the client sees.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d0dbfa99c0 |
fifa17-recon: the /settings gate bytes were never zero, and the plan built on that is dead
Yesterday's settings-gate plan asserted that IS_FRIENDLY_SEASON_ENABLED and
IS_DRAFT_MODE_ENABLED "have never been set to true by anything, on any run", and
proposed spending a launch on that premise. Measured against the running client,
both read 1, and so does packOpeningAnimationEnabled, while /settings has only ever
been answered {"configs": []}.
disp 0x1fd3a (friendlySeasonsEnabled) value = 1
disp 0x1fd3d (enableDraftMode) value = 1
disp 0x1fd45 (packOpeningAnimationEnabled) value = 1
Reproduced on two separate launches and two different pids.
Where the reasoning went wrong: the finding that FUN_18011dc50 is the only writer of
those bytes, and that the FutDataManagerImpl constructor never touches them, was
correct. The inference was not. The applier runs whether or not the configs array has
content, and the struct it is handed defaults these fields to 1, so the bytes were
being written all along. "Nothing populates the array" was treated as "nothing writes
the byte". Only the first of those was ever established.
Seasons therefore does not refuse because its gate byte is false. Its gate byte is
true. That diagnosis restarts, and the live test in section 3 should not be run as
written. The doc keeps the wrong turn on the record rather than quietly deleting it.
tools/gate_byte_probe.py makes this repeatable instead of a one-off. It is read-only
(O_RDONLY + pread), resolves the pid by comm, re-derives the CardsDLL slide from
/proc/<pid>/maps rather than caching it across launches, proves the slide against the
FNV prologue at 0x180180d00 read from the on-disk PE before trusting any address, and
decodes each gate displacement out of its accessor stub (0f b6 81 <disp32>) rather
than reading it from a table. Needs the client at the FUT hub, since CardsDLL loads
only then.
Also carries the two /settings changes that were pending from before: the mode
defaults to `off` (the live-proven baseline, since nothing here has faced the game)
and the transfer-pile probe is 77 rather than 100, because 100 is a stock-looking
number that would prove nothing if it showed up in game.
Live: 439 contract checks pass. check_settings_flags.py passes in all four modes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
897259c8fb |
fifa17-recon: the /settings 42-flag gate, and why Seasons never asks
ENDPOINT_MAP said this class reads one key, `configs`, and that was true and useless. What it missed is what happens after each element closes: the client feeds the STRING VALUE of `type` back through the atom hasher and switches on the result, 42 arms wide. A flag is a row, not a key, and the client hashes our string itself. Followed it to the end. FUN_18011dc50 is the only writer of the IS_* UI gate bytes inside FutDataManagerImpl, every line is `byte = (field == 1)`, and the constructor never touches those bytes. So a flag nobody sends is a gate nobody opens. friendlySeasonsEnabled and enableDraftMode have never been sent by anything, which is a mechanism for Seasons refusing while making zero requests to any of the four servers. The store is the control that makes this readable: IS_STORE_ENABLED is the same kind of byte and its screen works, because storeEnabled and friends already ship through the Blaze config store. That list has no seasons or draft flag. Ship the gates behind FUT_SETTINGS (off/keep/gates, default gates), and re-assert the working store flags in the same array on purpose: once a populated array makes the applier run, it writes EVERY gate byte, so omitting them could switch off a screen that works today. maximumTradePileSize=100 rides along as a positive control, because a boolean that changes nothing cannot distinguish "the flag did not help" from "the array never reached the consumer". check_settings_flags.py asserts each shipped name against the atom table AND the recovered switch, since a misnamed flag is silently inert and looks exactly like a failed fix. enableSquadBuildingSetsFeature is the reason both checks are needed: a real atom with no arm here. Live: 439 contract checks pass, market unit suite passes. Not yet tested in game. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ccf912c157 |
fifa17-recon: club items crashed the client -- unestablished fields, and too wide a blast radius
The game hung and then crashed on the first equippables fetch. My fault twice over.
CAUSE, primary. I copied teamid, leagueid and value straight out of the fcc row as
extras. `value` appears elsewhere as an OBJECT member (displayGroup {"value": ...}),
and a scalar where an object is expected is the type-desync busy loop at 0x1801c7f1a
-- which presents exactly as "the game is taking its time" and then dies. Omission is
safe; an unestablished field is not. That is this project's own rule and I broke it
for three fields that were not needed to draw a card. All three are gone.
CAUSE, contributing. The last request before the crash was type=equippables&count=11
and we answered with 30 items spanning FIVE unverified cardsubtypeids at once: the
widest possible blast radius for a wrong shape, and it tells you nothing about which
subtype was wrong. equippables now answers [] until the subtypes are confirmed one
family at a time, and shelf() takes a families= filter so a test can serve exactly one.
Adds FUT_CLUBITEMS=probe:<family>, which serves one item per candidate subtype for a
single family, so the screen names the correct subtype instead of me guessing a third
time. Eight items, one family, one question.
The flag already defaulted off, so a plain restart cannot serve any of this.
439 + 414 checks green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
f5002a0d4d |
fifa17-recon: serve club items -- the type names are SINGULAR and they were on the wire
Arming the counters made the client name the route within seconds, exactly as it did
for consumables:
club?type=equippables&count=11
club?type=stadium&count=200
club?type=ball&count=200
So it IS club?type= for this family, with SINGULAR names -- stadium and ball, not
stadia and balls -- and equippables as the combined club-customisation view. Those
requests were being answered from STORE.items(), which holds no club items because
the shelf is synthetic, so they correctly returned empty and looked like nothing was
happening.
Now serves the shelf: stadium 4, ball 6, badge 8, kit 8, equippables 30 (all four
combined), with player untouched at 205.
The cardsubtypeid values remain UNVERIFIED. cardtype 9 has no arm in the merge, so a
wrong subtype cannot announce itself; whether these render is now a live question and
the next screenshot answers it.
439 + 414 checks green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
3b58b29094 |
fifa17-recon: club-item counts, and packs that contain more than footballers
Correcting an overstatement first: I said every card family works. Balls, stadia,
badges and kits do not, and this is the start of that, not the finish.
CLUB ITEMS (FUT_CLUBITEMS=1, default off). tools/fut_clubitems.py builds shelves from
the game's own tables: fcc_balls 42, fcc_stadium 78, fcc_badgecards 656,
fcc_kitcards 1482, fcc_leaguelogos 44, each with its real carddbid AND its real
cardassetid. Counts are wired into the club stat set, replacing the honest zeros
rather than appending a second row per id (the deserializer does
store[ctx][statId] = value, so two rows for one id is a coin toss).
Counts FIRST and on purpose. The consumables round proved the count gates the fetch:
the client does not ask for an item list until club/stats reports a non-zero count,
and the CLUB tab reads 0x1e balls, 0x28 kits, 0x14 stadia. Arming the counters is what
makes the client name the item route, which is the one thing static reading has never
produced for this family -- cardtype 9 has NO arm in the merge, so nothing about it is
discoverable from the card DB.
What is deliberately NOT guessed: which cardsubtypeid means ball versus stadium. The
eight values that reach cardtype 9 are {30,31,145..150} and the assignment appears in
none of the 149 dumped tables. probe_shelf() serves one item per candidate so the
screen can say which is which, rather than shipping a guess into someone's club.
PACKS (FUT_PACK_MIX=1, default on). A pack is no longer eleven footballers: roughly a
quarter of each pack is now consumables and occasionally staff, as a ratio so it
scales from a 5-card bronze to an 11-card premium. Club items are excluded until their
subtypes are verified -- a pack is the worst place to discover a wrong subtype,
because the card lands in the save and has to be cleaned out by hand.
Also fixes the art id AT THE SOURCE. fut_consumables now sets cardassetid from the
fcc_ tables, so a pack-granted consumable renders correctly too. The earlier fix only
corrected the copy served by the club route, which meant packs would have dealt cards
with the green NOT FOUND box again.
Pack tests run against a COPY of the profile, after an earlier test persisted 15 cards
into the live save and had to be undone by hand.
439 + 414 checks green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
214be11202 |
fifa17-recon: the green NOT FOUND box was a wrong art id, cardassetid != carddbid
A player photographed a green tag under every consumable card. It is not a status label at all: external/ion_fut/artAssets/.../notfound.swf is the client's placeholder for an art asset it could not resolve, so the card was drawing 'no such artwork'. The cause is two id columns that look interchangeable and are not. In the game's own fcc_ tables a consumable row carries BOTH carddbid (5003001) and cardassetid (3), and the art is keyed on the small one. fut_store._item copies resourceId into cardassetid, which is correct for players and wrong for every other family, so the client looked up art id 5003001, found nothing, and fell back. Now mapped from data/tables/fcc_*.json, dumped read-only from the client's own database, so these are the game's ids rather than a guess: training 3, contract 7, healing 10, misc 45. The same table also gives balls 37, kits 35, stadium 36, badges 39, league logos 40, which is what the club-item family will need. Two earlier guesses at this badge were wrong and are recorded as such: it is not the untradeable flag (changing it left the tag untouched, and the record showed the flag had flipped) and not a loc-string failure. 439 + 414 checks green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
c41b1514d3 |
fifa17-recon: consumables are served tradeable, so the untradeable badge clears
A player pointed at a green tag under every consumable card. It is ours, not the client's: the deserializer sets a flag from (untradeableCount < count), we set untradeableCount == count, so every stack read as fully untradeable and drew the badge. UNTRADEABLE_COUNT and UNTRADABLE_COUNT are UI state keys in .rdata. In FIFA 17 a pack-opened consumable is normally tradeable, so this was our own data showing through rather than a rendering fault. Now untradeableCount is 0 and the served copy carries untradeable false. FUT_CONSUM_UNTRADEABLE=1 restores the old behaviour. TODO/CONFIRM: the exact badge text was not read, only its source. If the tag survives this change it is a different label and the untradeable theory is wrong. 439 + 414 checks green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
122b7b94d2 |
fifa17-recon: consumables are STACK records, not bare items
The route was right and the body was wrong. We served bare items, the client took the
response and inserted NOTHING (the live card map held only the 11 squad players), and
the screen stayed empty with no error logged anywhere. A 200 with a well-formed body
that the consumer silently discards is the worst failure shape there is, and it is the
third time this project has hit it.
FutConsumablesSearchServerResponse resolved from the RS4 literal 0x1802222f8:
factory 0x180130a10, vtable 0x180222200, deserializer +0x08 = 0x180130d10 (6873
chars). It takes itemData(0x16b) at the root like the club list, but its ELEMENT is
not an item. It is a five-atom wrapper and exactly one of the five carries the item:
0xbc count int
0xd7 discardValue int
0x16a item -> FUN_18013fe00, the item parser itself
0x287 resourceId int
0x362 untradeableCount int
Everything else goes to the value-skip handler, which is precisely why a bare item was
accepted and did nothing. It also explains the UI: FUT draws consumables as one stack
with a quantity, not as N cards, and the wrapper is that quantity.
Identical consumables are now collapsed by resourceId and counted.
439 + 414 checks green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
ccb736fa71 |
fifa17-recon: the consumables item route, found live -- GET club/consumables/<category>
The counter WAS the gate, and fixing it produced the request within seconds:
11:23:36 GET /ut/game/fifa17/club/consumables/training
11:23:40 GET /ut/game/fifa17/club/consumables/contracts
Neither is club?type=, and neither is the /consumables/%s template we had been
hunting in the binary. The client asks here, and it asks ONLY once
club/stats/consumables reports a non-zero count. Two rounds of item-shape work went
unrequested for want of a counter.
Worse, the path is a /club prefix, so it fell through to the generic route: the
consumables screen was answered with the 194-card PLAYER list. It asked for training
cards and got Cristiano Ronaldo.
Now routed above the generic /club, serving the shelf filtered by category. The
segment names come from the UI group table at 0x180203260; training and contracts are
CONFIRMED on the wire, the other five are from that table and matched case
insensitively, with singular "contract" accepted because the client has used both.
An unknown segment serves the whole shelf and logs loudly rather than showing an empty
screen, since a new spelling is a wire fact worth catching.
Per-category counts match the on-screen panel exactly: training 42, contracts 13,
fitness 6, healing 21, position 20, playStyle 24, managerLeague 0. That correspondence
is what makes an empty list distinguishable from a wrong mapping.
439 + 414 checks green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
2f8a6512db |
fifa17-recon: answer the CONSUMABLES panel with consumable counts, not player counts
The tab was empty because we answered the wrong question. The client asks GET club/stats/consumables 41 times a session; we replied with the PLAYER stat set (players 205, playersGold 189 ...), which that panel does not read. Confirmed on screen: seven categories present and selectable, every one reading 0. Now appends 14 consumables* rows counted from the SHELF. The shelf, not STORE.items(): the consumables we serve are a synthetic overlay never granted into the save, so counting the store gives fourteen zeros, which on screen is byte-identical to failure and would have made the experiment unreadable. Counts match the independently derived expectation exactly: 126 total, 21 healing, 7 player contracts, 21 player training, 3 player fitness, 20 position, 21 GK training, 6 manager contracts, 19 playstyle. Safe by construction: the vocabulary is an ATOM switch (FUN_18012fd40, 40 arms, default return 0), so an unrecognised name is inert rather than fatal, and the rows are APPENDED -- the player, nation and league rows that drive the working screens are untouched. Zero rows unless FUT_CONSUMABLES is armed. Default ON because answering the consumables panel with player counts is wrong by inspection rather than a judgement call. FUT_CONSUM_STATS=0 reverts. 439 + 414 checks green. The open question this sets up: whether a non-zero count makes the client request an item list at all. If it does, the log names the route and /consumables/%s is settled for free. If the numbers move and no request follows, the panel renders from counts alone and the 126-item shelf was never needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
21b2263e30 |
fifa17-recon: family flags -- FUT_CONSUMABLES=0 meant ON
os.environ.get returns the STRING "0", which is truthy, so anyone typing
FUT_CONSUMABLES=0 to turn the family off would have turned it on. "", "0", "off",
"no" and "false" now all mean off; any other value is the mode string ("1", "all").
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
0e4bc13db4 |
fifa17-recon: serve consumables, coaches and managers behind flags, default OFF
Wires the three families into club_route as a synthetic OVERLAY. Nothing changes by
default: with all three flags unset every route returns byte-identical bodies, checked
against the live 194-item save (club 194, type=player 194, league=53 drill-down 67,
hub clubPlayers 194, every staff stat 0).
FUT_CONSUMABLES=1 serve on type=contract|training|healing|development
FUT_CONSUMABLES=all ... and on an untyped club fetch with no team=/league=
FUT_COACHES=1 serve on type=headcoach|gkcoach|physio|fitnesscoach|staff
FUT_COACHES=all ... and on type=manager, the one staff request ever observed
FUT_MANAGERS=1 serve on type=manager|staff
WHY AN OVERLAY AND NOT A GRANT. Clearing the flag restores the real club exactly on
the next fetch, with no un-granting and no edit to a save a live client is holding
open. And Store.add_items() uses setdefault("id", ...), so items arriving with an id
already set do NOT advance nextItemId -- granting these would eventually collide two
id spaces. The four overlay bases (9.4e8 consumables, 9.5e8 coaches, 9.6e8 managers,
9.1e8 probe) are asserted disjoint from each other, from the save's 1e8 and from the
sweep's 9e8. The cost is that overlay cards cannot be quick-sold or moved.
WHY EACH FLAG IS TWO-VALUED. The tab-to-?type= binding is UNOBSERVED -- only
type=player, type=manager and type=custom have ever come from this client, and none of
the nine other arm names in FUN_18012ec50 has. So every club fetch now LOGS the arm it
was asked for while any flag is set. That is what makes an empty tab actionable: it
says whether the request reached us and under which name, instead of nothing.
THE MIRROR FILTER, and it is not optional. club_route applied its cardsubtypeid filter
only when `kind and kind not in ("player","custom")`. For type=player, for type=custom
(the by-league / by-team drill-downs) and for an untyped fetch it filtered NOTHING, so
the moment the club held a non-player item it would be served straight into the
players tab and the drill-downs -- and a manager carries nation, leagueId and teamid,
so he would have appeared as a footballer in exactly the MY CLUB rows that were only
just made non-zero. The player branch now filters cardsubtypeid in (0,1,2,3). Provable
no-op today: all 194 items in the save are cardsubtypeid 0.
Same hardening for the three counters that keyed on itemType == "player" (hub
clubPlayers, _club_stat_context, _club_stat_set) -> _is_player(), i.e. the CLIENT's own
definition from FUN_1800d8330. itemType is INERT on the wire (atom 0x173 is parsed into
a stack std::string in FUN_18013fe00 and freed; it never reaches the record), so keying
our own screens on it made their correctness depend on a field the client ignores.
FREE SECOND ORACLE: the five staff sub-type counters (0xb..0xf) are now computed from
the overlay. FUN_180094ce0's STAFF_EMPLOYED row is the +0x800 SUM over those five, so
the parent id 0xa alone could never move it. That number moves without the merge being
involved at all, which keeps "our club reports N staff" separable from "the client
resolved the card".
item_def() also learns consumables (gated): answering a consumable resourceId with
cardsubtypeid 0 makes it cardtype 0 -- no merge arm, no miss-fill, i.e. plausible
garbage -- and it carried the same hardcoded rareflag 1 as fut_store._item().
tools/test_card_families.py: 414 offline checks over the item BUILDERS. Deliberately a
separate suite -- test_fut_contract.py talks to a live server over HTTP and imports
nothing from the server's own code, which is what lets it certify a future non-Python
implementation; these must run against the working tree without restarting anything.
Each guard was mutation-checked: reverting the 219 fix, or letting coach_item accept a
miss-fill assetid, or letting consumable_item accept a dead zone, each fails the suite.
Suites after: test_fut_contract 439/0, test_match_rewards 61/0, test_card_families
414/0. utas_server was NOT restarted; this lands on disk only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
7b9a4fde15 |
fifa17-recon: /club honours ?team= and ?league= -- drill-downs showed the whole club
Reported live: a Cristiano Ronaldo card appearing under Chelsea and under Arsenal. The data was right and so was the client. teamid 243 really is Real Madrid in the game's own teams table, and all eight Ronaldo cards in the live CardsDb map read teamid 243. The fault was ours: club_route parsed only ?type= and ignored ?team= and ?league=, so clicking a club or a league in the club panel was answered with the ENTIRE 194-item club. Every drill-down therefore contained every player. Note which half was already correct: the club/stats COUNTS were fixed yesterday and were right (England 11, Premier League 17). It was only the item list behind them that was unfiltered, which is why this looked like a data bug and was not one. Now: team=5 gives 26 items, team=243 gives 20, league=13 gives 22, and the unfiltered club list is untouched at 194. 439 checks green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
b996c0673d |
fifa17-recon: sweep every staff table at once (t*@)
The manager sweep (subtype 4, ids 1-5000) came back with 5000 records at cardtype 2 -- confirming FUN_1800d8330(4)=2 live -- and NOTHING written: no name, no nation, no teamid, and no miss-fill either. Our sentinel rating 7, position 25 and attributes all survived. So either manager ids are not in 1-5000 or that branch keys off something the player branch does not. Guessing the id space costs a club visit per guess, so 't*@lo-hi' now fans all five non-player tables across one range in a single response: 4 managercards, 5 headcoachcards, 6 gkcoachcards, 7 physiocards, 8 fitnesscoachcards. One staff tab load tests 1000 ids against all five. The full table set, from the DLL's own strings: players, managercards, headcoachcards, fitnesscoachcards, gkcoachcards, physiocards, fancards, newcards -- plus a consumables family (contract, fitness, healing, position, training and playstyle modifiers, formation and league mods) and club items (badges, balls, kits) which are almost certainly NOT DB-resolved the way cards are. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
1b1c178c09 |
fifa17-recon: /club honours ?type= -- the STAFF tab was showing footballers
Observed live: the staff tab issues GET /club?year=2017&type=manager&count=200. club_route ignored the parameter entirely and answered every type with the full player list, so FUT displayed players as coaching staff. The filter is deliberately narrow. 'player' and 'manager' are the only values the client has ever been seen to send; 'custom' (the by-league and by-team drill-downs) and a missing type keep exactly the behaviour that is already proven on screen, because those drill-down counts were only just fixed and must not be disturbed. An unrecognised type is filtered rather than answered with everything, since answering an unknown question with the whole player list is the bug being fixed. We own no staff cards, so type=manager is [] today -- an empty item list, the same shape the itemData parser already accepts everywhere else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
c28cad281b |
fifa17-recon: sweep can target the non-player card tables
The merge FUN_180141660 dispatches on record+0x4c, which FUN_1800d8330 derives
from cardsubtypeid alone, and each branch queries a different table by
carddbid = record+0x18 -- the same field players use for playerid:
0..3 -> 1 players (live-proven) 5 -> 3 headcoachcards
4 -> 2 manager 8 -> 4 fitnesscoachcards
6 -> 10 gkcoachcards 7 -> 5 physiocards
9..b -> 7 unidentified absent -> 0x156 -> 0, no merge at all
So a 't<subtype>@' prefix on the sweep window probes any of them the same way
players were probed: 't5@auto:1-20000:5000'.
Only cardsubtypeid changes. itemType stays 'player' because the merge dispatches
on the subtype alone and the wire shape of a real staff item has NEVER been
observed -- across every logged session the client has only ever asked for
type=player and type=custom. Inventing a shape for an unobserved request is the
change class behind every freeze this project has had.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
b0bbc2a07f |
fifa17-recon: sweep auto-advance + the three-state oracle, live-proven
The oracle is three-valued, and all three fingerprints are now confirmed live
against a running client rather than read out of Ghidra:
NAMED our sentinel rating 7 survives and a real name appears. The id is
real, and teamid/nation/leagueId come back FILLED by the game
because we send them as zero.
placeholder rating 7 survives but the name is 'Jamal Blackman', team 0. The
players-table row exists and is an empty slot. This is the trap:
169193 does this and it was in VERIFIED_ASSET_IDS.
MISS rating 0x32, teamid 0x78d, nation 0xe, position 2, name ' '. That
is the binary's miss-fill, byte for byte, and it is exactly the
blank card photographed in a pack today.
Scale: 5000 candidates per response ingests cleanly; 20000 was served and then
silently not ingested (the map did not change at all), so the ceiling is between
them and auto chunks default to 5000.
Auto-advance: the client PAGES the club, so one visit yields several fetches.
'auto:lo-hi:step' hands out the next chunk per fetch. Item ids derive from the
candidate's offset in the WHOLE range, not its index in the chunk, so chunks never
collide and results accumulate across fetches for a single probe at the end.
sweep_collect.py accumulates into data/players.json and rejects the placeholder
name as a matter of course. Yield in 20000-24999 was 19 real ids per 5000, which
is why auto-advance matters: the real roster clusters in 150000-240000.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
87e5cd2e53 |
fifa17-recon: FUT_ID_SWEEP -- use the running game as the player-DB oracle
dbdata.dll is an anti-tamper decoy (getTableData returns a self-integrity blob), so the players table only exists inside the running client. But we do not need to unpack it: the client merges its own DB into every item we serve, keyed on resourceId & 0xffffff, and leaves the result in a map we can already read. So serve a RANGE of candidate playerids as a synthetic club, then read the map back with card_identity_probe.py. One club fetch classifies the whole window. Sends teamid/nation/leagueId as ZERO so the client fills the REAL values (the merge only fills zeros -- confirmed live: one playerid appears twice with two different nations, both ours). Sentinel rating 7, deliberately not 50, so the miss-fill (rating 0x32) can never be mistaken for a surviving sentinel. The window comes from a control FILE read per request, not just the env: a full sweep is many windows and restarting mid-session is what produced 'error connecting to FIFA 17 Ultimate Team' once already. Nothing is written to the save, so clearing the file restores the real club on the next fetch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
132a013b39 |
fifa17-recon: card identity is a DATA problem -- live probe proves the record model
Adds tools/card_identity_probe.py, a read-only /proc/PID/mem walk of the CardsDb card map that reports the identity the CLIENT resolved for every card it holds. Why it matters: identity never comes from us. The item-parser tail registers every parsed item into the map, and immediately before that FUN_180141660 -> FUN_180135890 queries the client's own local players table by resourceId & 0xffffff. On a hit it fills name/face and leaves our rating/position/attributes alone; on a miss it writes a fixed generic card (rating 0x32, teamid 0x78d, nation 0xe, position 2, attrs 1, name ' '). That miss fingerprint is exactly the blank card photographed in a pack today, so the chain is confirmed by live evidence and not only in Ghidra. First live run, 11 nodes, 0 failed reads, size counter agrees with the walk: Ronaldo/Messi/Suarez/Kroos/Hazard all resolve with real names, so every record offset derived statically (+0x18 resourceId, +0xb4 rating, +0x94 teamid, +0x148 nation, +0x146 position, names inline at +0xb8/+0xc8/+0xdd) is correct live. This makes card identity a pure DATA problem: serve real playerids. The probe is the bulk oracle for finding them -- N candidate ids served, one read classifies all N. Also flips FUT_STORE_DISPLAYGROUP to default on; it shipped off pending proof that the key does not switch FIFA17.exe to another tile render path, and it was then run live and the store tiles showed their real names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
7ad7aa0afa |
fifa17-recon: club stats -- per-NATION buckets, and the sub-type sums
LIVE 2026-08-04: the ENGLAND tile read 0 while drilling into it showed Premier League
17. Same bug as before, one level up: the LEAGUE buckets were keyed and the NATION
buckets were not.
The eight-row MY CLUB panel is FUN_180094ce0 (not FUN_180043b90, which is a different
provider using a different string family), and it computes:
PLAYERS_EMPLOYED = +0x7f8(nationId, 4) + (nationId, 3) + (nationId, 2)
STAFF_EMPLOYED = +0x800 over 0xb, 0xc, 0xd, 0xe, 0xf
TROPHIES_WON = +0x800 over 0x33 .. 0x38
STADIA_OWNED = +0x800(0x14) BALLS_EARNED = +0x800(0x1e)
Two consequences:
1. The no-id modes (year / consumables / club / newcards), which is what the client
fires on entering MY CLUB, now carry PER-NATION buckets keyed by nation id. The
three screens are consistent at last:
no id -> nation buckets (the tab strip and the eight-row panel)
country/<id> -> league buckets (the leagues in that nation)
league/<id> -> team buckets (the teams in that league)
2. STAFF_EMPLOYED and TROPHIES_WON are SUMS OF SUB-TYPES. Sending staff(0xa) or
trophies(0x32) alone can never move those rows, whatever their value. The eleven
sub-type rows are now emitted: staffManager/HeadCoach/GKCoach/Physio/FitnessCoach
and trophiesOffline/Online/FeaturedOffline/FeaturedOnline/SeasonOffline. All zero
today because the club owns no staff and has won nothing, but the mapping is what
matters when it does.
All eleven new type strings verified against docs/fut_atoms.tsv, 0 mismatches.
Live: /club/stats/year now returns 117 rows across 16 nation buckets, England
(nation 14) summing to 11 players. 439 + 61 checks green, zero tracebacks.
This is the third correction to this one endpoint today. The pattern in all three is
identical and worth stating once more: the parser accepts anything, and only the
CONSUMER tells you which bucket and which type ids it reads. Every time I reasoned
about the body instead of reading the reader, I shipped a wrong one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
4c5cc3ab4b |
fifa17-recon: THE MY CLUB COUNTER -- it was clubPlayers in GET /hub all along
The tile's big number is `clubPlayers` (atom 0x90) in the body of GET ut/%s/hub, a
route we have answered with {} for the life of the project.
The chain, re-derived independently by two agents (one via Ghidra, one via raw PE plus
capstone with no decompiler) and checked by two reviewers:
clubPlayers(0x90) --INT getter 0x1801c79d0--> clamp FUN_1800d7b30 (<=0 becomes 0)
-> R+0x3c, where R = FUT data-manager slot +0x1f8 (FUN_18011a810 is literally
`lea rax,[rcx+0x1fd70]; ret`)
-> read by FUN_1800b0250, published as TEXT0 of TILE_ID 0x210
-> captions FUT_GH_TOTAL_PLAYERS_0/_1 at 0x18020a0f8 / 0x18020a110
auctionCount(0x33) -> R+0x38 -> TEXT0 of TILE_ID 0x1b0, the TRANSFERS tile
FUN_180139610 (the hub body parser, 14855 chars, censused in full: 18 atoms, none
missed) is the ONLY writer of +0x3c anywhere in the image, one write, guarded by
`if (iVar6 != 0x90)`. This is not a candidate, it is the field.
I SPENT A DAY ON THE WRONG SURFACE AND WROTE THE WRONG CONCLUSION. REBUILD_RESEARCH
S19 declared the counter "not server-fixable" with a mechanism that was internally
correct and completely beside the point: the tile never read the club-stat store.
Two things reinforced the error and both are now fixed in the docs:
* ENDPOINT_MAP said this response "uses C++ reflection / vtable dispatch, NOT an
inline atom ladder -- no static field ladder to read" and marked it a GAP. False.
There is an inline ladder, one indirection away.
* The eight-row MY CLUB panel was assumed to be FUN_180043b90 case 1, which
publishes six keys, and I treated the six-versus-eight mismatch as a puzzle rather
than as evidence. It is a DIFFERENT provider, FUN_180094ce0, using a different
string family (FUT_MYCLUB_*), reading neither the mode tag nor any type id we were
sending. Two providers; we were reading the wrong one.
That is the third negative claim of this shape to fail today, after "this deserializer
has no skip handler" and "the factory does not wipe the stat map".
auctionCount is included as a FREE CONTROL: different field, different tile, so if MY
CLUB moves and TRANSFERS does not, delivery is fine and something is specific to +0x3c.
Default ON. Freeze risk is low by construction rather than by belief: a flat object of
two integers, both read with the INT getter, so there is no array, no nested object and
no type-desync surface. FUT_HUBDATA=0 restores {}.
Contract guard added, and verified to bite rather than merely pass:
default 439 checks, 0 failed
FUT_HUBDATA=0 435 checks, 3 FAILED (clubPlayers missing / not a number)
A regression here would otherwise be silent: still 200, still valid JSON, tile quietly
back to 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
ffb0e6033c |
fifa17-recon: real card pool -- 79 players, three tiers, and packs that differ
The pool was 18 players rated 85 to 94. open_pack() split it with `(rating >= 75) ==
gold`, so the bronze pack's filter matched NOTHING and fell back to the whole pool:
all three packs dealt gold rares and the bronze pack was a lie. The file's own TODO
asked for "a full dbdata.dll extract (~18k players)".
NEW fut_cards.py: 79 players, 39 gold / 20 silver / 20 bronze, 7 leagues, 20 nations,
18 teams, every outfield position plus GK, no duplicate asset ids. PACK_CATALOG now
carries a weighted `tiers` draw per pack. Simulated 40 opens of each:
Bronze Pack {'bronze': 159, 'silver': 41} 39 distinct cards, 12 positions
Gold Pack {'silver': 110, 'gold': 170} 59 distinct cards, 12 positions
Premium Gold {'gold': 392, 'silver': 48} 57 distinct cards, 12 positions
WHY THIS IS NOT THE dbdata EXTRACT, and why that does not matter yet. dbdata.dll is a
real PE with one export, getTableData, whose 2.5MB payload sits in a section named
.xdata that disassembles as obfuscated code rather than a table directory, so the base
DB is not statically extractable without running that export under Wine or defeating
the obfuscation.
More to the point it would change nothing on screen today. Per docs/CARD_SYSTEM.md the
card view-model 0x1800d7920 reads EVERY rendered field (rating +0xb4, position +0x146,
nation +0x148, teamid +0x94, six attrs +0x98..0xac, name +0xdd) from a definition
record resolved at item+0x10 out of the client's own CardsDb map, and NEVER from our
item JSON. Offline that map is EMPTY, so every lookup misses and a blank record is
emitted. No assetId we send, real or invented, can produce a named card until that map
is populated. That is a separate job (CARD_SYSTEM options A/B/C) about the CLIENT's
map, not about our pool.
What the pool DOES control is everything the server is source of truth for: the
gold/silver/bronze split, leagueId/nation/teamid which are exactly what the club-stats
drill-downs read (those now work, S20, and were being fed 5 leagues from 13 nations),
preferredPosition which decides whether a squad can be filled at all, and the six
attributes behind the market filters.
Asset ids: 18 are genuine FIFA 17 ids and are listed in VERIFIED_ASSET_IDS. The rest
are structural, and the docstring says so plainly rather than passing them off as real
players. Because the CardsDb map is empty offline an id being wrong has no visible
effect today; if the identity work lands, that set is the diff target.
Backwards compatible: open_pack() still honours the legacy `gold` boolean when no
tiers are given, and the old list survives as _LEGACY_POOL for the starter squad.
392 + 61 checks green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
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
|
||
|
|
f65c197942 |
fifa17-recon: club stats -- key the buckets the way the READER looks them up
Second correction in an hour, and this one comes from reading the provider instead of
reasoning about it. The per-context getter is (+0x7f8)(store, contextValue, typeId),
and contextValue comes from THE UI ROW, not from the URL:
case 3: uVar7 = (**(param_2 + 0x18))(param_2, row, "LEAGUE_ID")
bronze = (+0x7f8)(store, uVar7, 2)
silver = (+0x7f8)(store, uVar7, 3)
gold = (+0x7f8)(store, uVar7, 4)
publish "PLAYERS_EMPLOYED", gold + silver + bronze
rare/kits/badges = (+0x7f8)(store, uVar7, 5 / 0x28 / 0x2d)
case 4: keyed by "TEAM_ID"; reads 1 (players), 0x28 (kits), 0x2e (badgeDBid)
Three things my previous commit got wrong:
1. It keyed every row to the id in the URL. The reader iterates the SCREEN'S ROWS and
looks up each row's own id, so one response must carry a bucket per row. Keying to
the URL id fills exactly one bucket the screen never asks for, which is why the
ENGLAND tab still showed zeros after the "fix".
2. PLAYERS_EMPLOYED is COMPUTED as gold + silver + bronze in the per-context cases and
is never read from the store, so sending `players` (type id 1) does nothing there.
The tier counts are mandatory.
3. The screens NEST: country/<id> lists the LEAGUES in that nation (case 3, LEAGUE_ID)
and league/<id> lists the TEAMS (case 4, TEAM_ID). That matches the live navigation
exactly: selecting ENGLAND produced Premier League / Championship / League One /
League Two.
Because every response wipes the whole map, each response only needs its own screen's
buckets, which also avoids a real collision: the storage key is contextValue alone, so
nation 14 and league 14 would otherwise share a bucket.
Live output now:
country/14 -> 41 rows, 5 league buckets
league 13 gold=17 -> PLAYERS_EMPLOYED=17 (Premier League)
league 19 gold=26, league 53 gold=55, ...
league/13 -> 6 team buckets {5:20, 21:15, 22:11, 240:21, 241:30, 243:17}
Recorded as a method note: two rounds of reasoning about this endpoint produced two
wrong bodies, and reading twenty lines of the provider produced the right one. The
question "what does the reader look up" is answerable and was not asked.
392 + 61 checks green, zero tracebacks. Still behind FUT_CLUBSTATS, default off.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
9ee21afb56 |
fifa17-recon: club stats -- populate the PER-CONTEXT buckets, not just the global one
LIVE 2026-08-04, and this corrects the body I shipped an hour ago. Selecting the ENGLAND tab on the MY CLUB screen issues exactly one request: 14:30:32 GET /ut/game/fifa17/club/stats/country/14 (14 = England) and NO item-list request. So that tab is driven entirely by per-nation stats, and it showed nothing while the club holds 8 England players. THE GUARD WAS THE BUG. In deser 0x180130150, contextId == 1 or 5 <= contextId <= 9 FORCES contextValue to 0, which is the global bucket that the +0x800 getter reads. The per-nation view reads the +0x7f8 getter keyed by the NATION ID instead. Every row I sent carried contextId 1, so no matter what contextValue said, everything landed in the global bucket and the per-context tabs could never see it. I had the guard written down in my own comment and still sent a body that tripped it on every row. Now, for country/<id>, league/<id> and team/<id>, the response carries the global rows AND per-context rows keyed by that id, computed from the club's real nation/leagueId/ teamid fields: country/14 -> players 8, playersGold 8, rarePlayers 8, silver/bronze/kits/badges 0 Both sets ride in the SAME response because every response wipes the whole map first, so anything left out is erased rather than merged. contextId 3 is used purely because it is OUTSIDE the guard and therefore preserves contextValue. TODO/CONFIRM what contextId means semantically; nothing read so far gives it a meaning beyond that guard. Also verified rather than assumed this round: all 11 type strings we emit resolve correctly against docs/fut_atoms.tsv (players 0x238, rarePlayers 0x272, stadia 0x2d7, balls 0x4f, kits 0x17c, badges 0x4b, trophies 0x340 ...), 0 mismatches. So the strings were never the failure. Does NOT claim to fix the MY CLUB hub counter, which remains the open question in S19. This fixes the nation/league tabs, which is a different and now-understood symptom. 392 checks green. Still behind FUT_CLUBSTATS, default off. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
1e2b073e04 |
fifa17-recon: route the draft entry purchase, and resolve the envelope ambiguity
LIVE 2026-08-04: the draft-state array fix WORKED. The screen rendered instead of
hanging and the client advanced to the entry-fee screen, then crashed on the next
call, which we had never implemented:
GET /squad/mode/draft/state?mode=ONLINE -> our array body screen RENDERED
GET /user/credits -> 7200
GET /store/purchasegroup/all -> the entry-fee screen
POST /purchase/mode/0/draft {"currency":"COINS","usePreOrder":0}
-> {} UNMAPPED, then the crash
Advancing the failure to the next unimplemented call is what a correct fix looks like.
ENVELOPE AMBIGUITY RESOLVED, and ENDPOINT_MAP's note about it is wrong. Two structures
reference RS4:FutPurchaseDraftModeServerResponse:
0x18014c260 vtable 0x180224ef8, factory 0x18014c090. 3188 chars, OBJECT root
(prologue tests != 10 = END_OBJECT), 1 skip handler, exactly the seven
scalar ints. THIS IS THE RESPONSE PARSER.
0x180150310 vtable 0x1802262f0, factory 0x180150260. 1836 chars, ARRAY root
(loops until 0xd), ZERO skip handlers -- and NOT a response root at
all. It parses ENTRANCE CRITERIA: each element's name is strcmp'd
against the literals "COINS", "POINTS", "DRAFT_TOKEN" and stored at
+0x28/+0x2c/+0x30. It shares the class-name string because it is the
fee sub-object, not an "alternate/summary envelope" as documented.
THE CRASH ITSELF DISCRIMINATED, which is worth keeping as a technique. An object-root
parser handed {} parses benignly and leaves defaults; an array-root parser handed {}
desyncs and HANGS, which is exactly what draft/state did before the fix. We observed a
CRASH, not a hang, so the object-root parser is what ran and the failure is downstream
of an empty-but-valid parse. Consistent with 0x18014c260, inconsistent with the other.
Coins are NOT deducted. The client posts the price in the URL and it sent 0, because we
omit entranceCriteria from draft/state so there is no fee to charge. Charging a guessed
amount would be inventing an economy rule.
ALSO FIXED, before it reached the game: the route table hands handlers the compiled
PATTERN, not a match object (the dispatcher calls fn(rx, self)), so calling .group() on
the first argument raised AttributeError and killed the connection outright. That is
strictly worse than the {} it was replacing. Caught by verifying the response actually
changed after the restart rather than assuming the route worked.
Default ON: the behaviour it replaces is a confirmed crash, so no working state is at
risk. FUT_DRAFT_PURCHASE=0 reverts.
392 + 61 checks green, zero tracebacks on a clean boot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
b434a3efdc |
fifa17-recon: FUT_CLUBSTATS -- serve the club-stat set (CLUB STATS panel, maybe the tile)
Live 2026-08-04: the CLUB STATS panel shows eight zeros (Rare Players, Players, Staff
Employed, Stadia Owned, Trophies Won, Kits, Badges, Balls Earned) while the client
fetches /club/stats/{staff,year,consumables} and we answer {} to all three. Those
zeros are ours. Every row name maps to a type string in the recovered map.
Wire schema, fully verified from deser 0x180130150 (7,870 chars, read end to end):
{"stat":[{contextId:int, contextValue:int, type:string, typeValue:int}]}
Unknown keys route to FUN_180135ff0 at BOTH levels, so extras are inert.
FIVE THINGS THAT DECIDE WHETHER IT WORKS:
1. EVERY RESPONSE WIPES THE WHOLE MAP FIRST. Nothing accumulates, so a good body on
one mode followed by a thin one on another ERASES the first and request ordering
decides what survives. Handled by serving the SAME COMPLETE SET on every Stats2
mode: whichever lands last leaves the map correct. (One investigator reported this
factory does not wipe; a reviewer re-read it and refuted that. The wipe is real,
and this is the second negative claim from that batch to fail.)
2. /club/stats/staff IS A DIFFERENT CLASS: FutStaffBonus, {"bonus":[{type,value}]},
not Stats2. Its type strings are undecoded so it keeps {}, which is safe and also
means it does not disturb the Stats2 map.
3. ELEMENT-LOCAL VARIABLES ARE NOT RESET BETWEEN ELEMENTS -- the clears sit before
the array loop, not inside it -- so omitting a key in element N inherits element
N-1's value. All four keys are emitted in every element.
4. The storage key is contextValue ALONE; contextId is only a guard (1, or 5..9,
forces contextValue to 0, the global bucket the +0x800 getter reads). contextId 1
throughout.
5. 0x3d CONTRACTS, 0x3e TRAINING and 0x40 FITNESS are READ by the panel but cannot
be SET from here. No type string produces them.
THIS IS ALSO NOW THE HUB-TILE CANDIDATE. The investigation concluded the MY CLUB tile
does not read this store, but flagged that negative as BOUNDED: the interface comes
through a QueryInterface adapter, so the vtable is assembled at runtime and cannot be
read statically. Live evidence points the other way. The tile reads "0 TOTAL PLAYERS"
and the panel reads "Players 0" -- same quantity, both zero, both while we answer {}.
And FUT_CLUB_PAGE ruled out the alternative: 114 items served to /club, tile still 0,
so it is not a count of the list. Strong inference, not proof; this flag is the test.
The test is unusually clean: the club holds 114 items and all of them are players, so
every other row is an honest zero. If it works, exactly two numbers move (Players and
Rare Players, 0 -> 114) and nothing else changes.
Gold/silver/bronze thresholds are FIFA's rating convention (75+/65-74/under), not
something read out of the binary, and the code says so.
Default OFF. 392 + 61 checks green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
9cf21202dc |
fifa17-recon: delete two loaded guns, fix the season root, add the tile-name fix
ZERO-LAUNCH FIXES from the multi-agent pass over 0x18013af30 and 0x1801683f0.
FUT_STORE_FIELDS IS DELETED, AND IT WAS A FREEZE, NOT A REGRESSION. It sent
"actionType": 0 and "firstPartyStoreId": 0 as JSON INTEGERS. Both atoms (0x8, 0x127)
are read with the STRING getter 0x1801c7aa0. That is the exact type-desync class this
project exists to avoid. So "the corrections stopped packs opening" was never bad luck
or an unrelated field: two of them were the documented freeze mechanism, shipped by a
change whose own comment claimed it was correct about what the parser reads. Knowing
WHICH atoms a parser reads tells you nothing about which TYPES it demands. Read the
getter, every time. (Two other fields in that block were no-ops anyway: useDefaultImage
0x36a stores inverted, and visible 0x37d never reads its value at all.)
FUT_STORE_GROUPS IS DELETED and its freeze is now traced end to end. It sent
displayGroup as an ARRAY of pack-shaped objects on the belief that the key was parsed
recursively by the same element parser. It is not recursive at all: case 0xd9 never
re-enters 0x18013af30. It is a FLAT OBJECT with exactly two members. An array desyncs
the reader, the parser runs off the end of the document, and the tokenizer returns the
same token forever with nothing consumed, spinning inside FUN_1801c7f10 whose body
contains the observed PC 0x1801c7f1a.
Both were being kept as togglable "maybe nearly right" experiments. Each is a loaded
gun; neither survives contact with the decompile. Deleted rather than left switched off.
NEW FUT_STORE_DISPLAYGROUP (default 0): send the one key that actually names a tile,
displayGroup = {"value": "<pack name>"}. `value` (0x377, STRING) writes record offset
+0x00, the same slot whose constructor default is the literal "unknown" (the only such
literal in the DLL, 0x180223108). The tiles say "unknown" because nobody ever sent the
field. Distinct values per pack so grouping stays 1:1. priority/displayGroupAssetId/
displayGroupUseDefaultImage all omitted as second variables.
Default OFF for a reason the token-balance proof does not cover: this may be the first
field we have sent that selects a RENDER PATH rather than a value, and that code is
packed.
SEASON ROOT SHAPE CORRECTED. season_list() and its docstring were both wrong in the
same way: the deserializer is 0x1801683f0 (object root, one key seasons=0x2ad, array
inside), not 0x180167740, which is the per-ELEMENT parser. Someone read the element
parser and served its key set at the document root, so a bare array populated nothing.
Three of the keys served (eligibilityKey/Slot/Value) are inner members of elgReq and
inert even at element level. Also recorded: `type` must precede `divisionId` because
the divisionId branch reads the parsed type at elem+0x1b4.
Still behind FUT_MODES and still pointless to serve: across 486 real client requests
the game has NEVER asked for /season. Every /season line in our log is our own curl.
NEW FUT_CLUB_PAGE (default 0): an experiment, not a fix. The MY CLUB counter's
renderer is not in cardsdll (no two-number formatter of any spelling exists) and
FutStickerBookSearch has no count atom, so there is no field we can send that IS the
number. What is still testable is whether the counter is a Flash-side count over the
returned list, which a pure length change discriminates. A NULL RESULT IS THE VALUABLE
ONE: if the counter does not move, there is no server-side lever for this symptom and
the right outcome is to prove that and stop.
392 + 61 checks green, defaults byte-identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
24bbc32da5 |
fifa17-recon: fix the match tail -- real URLs, endReason, and coins in the right place
The whole match family is one RPC descriptor block (rows 49-54, every row using URL
template index 16 = `ut/%s/match`) with a fixed suffix appended per call:
CREATEMATCH ut/game/fifa17/match PLAYGAME ut/game/fifa17/match
MATCHREADY ut/game/fifa17/match/ready DESTROYMATCH ut/game/fifa17/match/end
RESETMATCH ut/game/fifa17/match/reset KEEPALIVE ut/game/fifa17/match/keepalive
THERE IS NO /match/{id} URL. The id travels in the body. Our reward path was gated on
`h.command == "DELETE" or "/ut/delete/" in h.path` and extracted the id with
re.search(r"/match/(\d+)"), so it was waiting for a request the client does not make.
The gate is now widened to include a /match/end path with ANY verb, because the verb
genuinely cannot be determined statically: the strings "PUT" and "DELETE" do not exist
anywhere in cardsdll.dll (0 hits each), so verb selection happens outside this DLL. A
reviewer flagged "the reward path can never fire" as overreach on exactly that point;
widening rather than replacing the gate is the response.
THE RESULT SIGNAL IS `endReason` (atom 260), a STRING enum with nine values: WIN DRAW
LOSS DNF QUIT NO_CONTEST DNF_WIN DNF_DRAW DNF_LOSS. Not a score comparison. The score
lives in `myMatchStats.goals` / `opponentMatchStats.goals`, two literal-keyed objects
of 15 int fields each, and the client OMITS both when endReason is DNF or QUIT, so
nothing may require them. _match_result() now reads endReason first and keeps the old
spelling probe only as a fallback, because request-side static findings are a floor:
PUT /item's swap/tradeId appeared in no static listing either.
THREE CORRECTIONS TO THE RESPONSE, all of which were shipping wrong:
1. `coins` (atom 149) is NOT a top-level key. It is read only inside `gameModeAward`.
The one field most obviously named "the reward" was being silently skipped.
2. `qualifiedChampionEventId` (0x269) has a SIDE EFFECT: its branch calls through a
manager vtable after storing. Sending a habitual zero poked champion-event
machinery for no benefit. Removed.
3. `bidTokens` (atom 89) inside gameModeAward is MATCHED and then handled by nothing,
so its value token is left unconsumed. That is the precondition for the desync
spin. A freeze trap dressed as an ordinary field; now guarded by a unit check.
test_match_rewards.py rewrote its expectations. The old version asserted a top-level
`coins` and passed happily while the server shipped a body whose reward field the
client never read. A test that encodes the wrong schema converts a bug into a
guarantee. New test_end_reason_is_authoritative covers all nine enum values, the
stats-less DNF case, and that endReason beats a contradictory score probe.
DEFAULT ON (FUT_MATCH_END=0 reverts), a reasoned exception to the flag convention:
nothing here is live-proven because no match has ever been played, and the old
behaviour is not a working screen but a path that provably could not fire.
61 unit checks (58 with the flag off), 392 contract checks green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
0968cd351b |
fifa17-recon: route squad/mode/draft/state -- the root container is an ARRAY
Deser 0x180147070 (FutGetDraftCurrentStateServerResponse) discards tokens until it sees START_ARRAY. Handed a top-level OBJECT it never reaches its exit condition and spins in the inner `while (tok != END_OBJECT)` loop while the tokenizer returns EOF forever. Process alive, no crash dump, no dialog: exactly the signature observed live on 2026-08-03, when our generic /squad route answered this endpoint with a full active-squad object. The suffix composition `ut/%s/squad/mode` + `/draft/state?mode=...` makes the URL invisible to the request-template table, which is why /squad swallowed it. Fourth time a suffix endpoint has been invisible to that table, second time the generic /squad route has eaten one (/squad/list was the first). Verified at instruction level rather than by regex: 7 top-level atoms (4 int-getter calls, 3 string-getter, 0 bool, 2 skip) across the whole body [0x180147070, 0x1801475a3]. FUN_180135ff0 IS present, twice, so unknown keys are inert. NO ATOM COLLIDES with the squad object we were serving, which means the hang was purely the container level and not a per-field type desync. entranceCriteria(0x108) is now known to be an object of three int keys COINS/DRAFT_TOKEN/POINTS. It is OMITTED anyway: knowing a shape is not a reason to send it. A second agent independently simulated this exact body through the deserializer line by line and got a clean exit in 16 token reads, and separately refuted four claims in the first agent's report (a census undercount, a wrong .rdata address where 0x18021e7f4 is 'TFA' not the squad template, an incorrect stateParam2 typing argument, and a dangerous aside about a second array-root envelope). The body survived all of it. DEFAULT ON, a deliberate exception to "default to the live-proven value": the live-proven value here HANGS THE GAME, and there is no working screen to protect because Draft cannot be entered at all today. FUT_DRAFT_STATE=0 restores the old routing. 392 + 51 checks green; /squad/0 and /squad/list verified unchanged. 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
|
||
|
|
a93a8bcdd7 |
fifa17-recon: decouple contract suite from the Python implementation
test_fut_contract.py no longer imports ACCOUNT from fut_account. The expected persona comes from FUT_TEST_PERSONA_ID and the target from FUT_TEST_BASE, so the suite now imports nothing but stdlib and talks to a server at a URL. That is what lets these 380 checks certify ANY implementation of the reversed spec, a future Rust openfut-core included, without replaying the reverse engineering. The original reason for reading ACCOUNT still holds and is preserved in the comment: suite and server must not each hold a private copy of the constant, or the identity-consistency checks would only prove two copies matched. Also adds pileSizeClientData(0x227) behind FUT_PILESIZES (default off). A probe run with 16 uniquely-valued entries did NOT move the MY CLUB counter, so that member is eliminated as its source; the code is kept for the record and flagged off. Docs: OPENFUT_PROJECT_REPORT.md and OPENFUT_HANDOFF.md. The report now separates "built but untested" from "never requested by the client" -- the server log records User-Agent, and splitting real client traffic (ProtoHttp) from this project's own probes shows /season, /tournament, /champion, /match, /clubUser and /user/list are at ZERO client requests. /clubUser (0 client, 93 probe) and /user/list (0 client, 180 probe) are the starkest: work was done on both assuming the client wanted them. 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
|
||
|
|
59934b4ef0 |
fifa17-recon: FUT squad blocker solved + userInfo delivered
The client now issues PUT /squad and the hub renders coins, record and the
squad roster. Three separate root causes, all verified live.
Squad blocker (the long-standing "client never sends PUT /squad"):
AddPlayerToSquad, GetSquads and SelectSquadById issue ZERO network requests
(pure local model reads/mutations, FutSquadServiceImpl vtable 0x180233ff0);
only SaveCurrentSquad writes, and it is unguarded. The client simply needed a
populated ACTIVE squad model, which arrives via the massinfo `squad` member.
No response of ours was ever being rejected.
userMassInfo is NOT required to be {}:
0x180174630 is a FLAT {userInfo, squad, settings, userData} body -- the old
"wrapper key is user" note was wrong, and the historical freeze was the
malformed squad member, not the envelope.
clubNameChangeAllowed must be false:
sending true advertises a club-rename flow whose UI model is never populated;
the client shows a naming prompt and dies confirming it (ACCESS_VIOLATION
reading 0x0 at FIFA17.exe+0x71b8651, 4/4 runs, no CardsDLL frame and no request
in flight). Isolated by a single-variable run; guarded by a contract check.
Endpoint/schema corrections found in live traffic, invisible to static analysis:
* GET ut/%s/squad/list is a real endpoint and must return {"squad":[...]},
not the active-squad object (the /list suffix is appended by the caller, so
it never appeared in the request table)
* PUT lands on ut/%s/squad/<id>, not a bare ut/%s/squad
* userInfo currencies are read as name/funds/finalFunds/active -- there is no
"value" key, so coins always rendered 0
* squad-list elements take STRING formation/squadType, not ints
* the CardsDLL script-API thunk<->name table was off by one (AddPlayerToSquad
is 0x18004aa70; 0x18004aff0 is GetPotentialChemistry_Club)
FUT_MASSINFO / FUT_USERINFO ladders keep every step of the bisect reproducible.
Contract suite 311 -> 358 checks. Tooling added: PyGhidra harness (Ghidra's
Java/OSGi script path is broken on this box), minidump reader, live code grabber.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
f9ffcfdf20 |
fifa17-recon: map /transfermarket (live: FIFA's market search endpoint)
Live log ground-truth: FIFA's transfer-market SEARCH hits
GET /ut/game/fifa17/transfermarket?type=player&start=0&num=12 (was UNMAPPED ->
catch-all {} => empty market), NOT /auctionhouse as the struct name suggested.
Route it to the same listings handler. Market now serves 18 listings on the
endpoint FIFA actually calls.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
|