fut_store._item() hardcoded "rareflag": 1 on every item it builds. That is inert
for players and for staff, and CORRUPTING for exactly one consumable subtype.
MEASURED, in the binary: FUN_1801bfac0 case 5 (consumable category 5, fitness)
takes the squad-fitness branch when
(cardsubtypeid == 0xdc) || FUN_1801a88c0(rec)
and FUN_1801a88c0 is exactly `*(int *)(rec + 0x58) == 1`. rec+0x58 is the rareflag
atom 0x271 (FUN_18013fe00 case 0x271 -> uStack_130; the frame arithmetic is
independently pinned by local_138 -> rec+0x50 and local_13c -> rec+0x4c, the two
offsets card_identity_probe has been reading live for days). FUN_180141660 does not
overwrite rec+0x58 for cardtype 6 -- cases 6/7/8/9 fall to the shared tail, which
writes only rec+0x54 -- so a rareflag we send survives all the way to the render.
Result: subtype 219 with rareflag 1 draws FUT_CONSUMABLE_NAME_SQUADTRAINING with
artwork 5000011 instead of Player Fitness with 5000010, and forces the
single-target count at param_5+0x1bc to 0. Silent. It would have corrupted the
first fitness card we ever served.
The guard is `0 if cardsubtypeid == 219 else rareflag`, added with two new KEYWORD
params. Every existing call site (fut_store.py:74/:357, utas_server.py:1404/:2136)
passes 8 positional args, so both take their defaults and the player dict is
byte-identical -- key order included, asserted in tools/test_card_families.py.
Scope correction to the round's own notes: rec+0x58 is read TWICE in that
42,813-char render function, not once. FUN_1801a88c0 is the category-5 read, but
line 108 reads it directly into param_5+0x1f0 (the rare/backing art) for EVERY
cardtype, before the `if (param_4 == 6)`. So the guard also stops a 219 being drawn
as rare -- intended, since rare IS the squad-fitness selector.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
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
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
Complete the market loop (browse + buy + sell). POST auctionhouse (FutISStart)
lists an owned club item -> profile.listings + returns {id:tradeId}. tradePile
builds a validated auction record per listing from the owned item + prices
(freeze-safe, same 0x18013e410 shape). DELETE trade/{id} removes the listing.
fut_store gains list_for_sale/listings/remove_listing (tradeId space 900500000+).
test_market_buy.py extended with sell/delist checks (temp profile, no real-save
mutation): list -> tradePile shows it with prices -> delist empties it. All pass.
Read-only contract suite still 311/311. Functional (FIFA's exact sell params)
pending live test; freeze-safe by construction.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
FIFA's updateActiveSquad PUT stores each slot as itemData={id:<clubItemId>}
(a reference), not the full player. We saved the bare references, so the
squad reloaded empty ("active squad resets"). Add Store.reconstruct_squad()
to re-embed the full club item by id on GET /squad/0, so the saved squad
reloads with its real players.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
Add a first-cut FUT store on top of the persistent profile:
- fut_store: PACK_CATALOG (Bronze/Gold/Premium), a curated real-player
PACK_POOL, and open_pack() (deduct coins -> generate items -> add to
club -> persist).
- utas_server routes: GET store/purchasegroup/all (catalog), PUT
(v2) store/transaction (buy + open, returns awarded itemData +
updated coins), GET purchased (last pack). Matches both /ut/game and
/ut/v2/game prefixes.
Verified via curl: buy Gold Pack -> 7 real players awarded, coins
15000->10000, club 10->17, persisted. Wire format is a best-guess
grounded in the CardsDLL store keys (packId/price/itemData/coins);
iterate against the in-game store next. Pool is curated for now --
replace with a full dbdata.dll extract later.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
Add fut_store.py: a JSON-backed profile store (coins, owned club items,
saved squads, record). First run grants a starter pack -- 15000 coins +
a 10-player starter club (real assetIds; identity resolves locally
in-game per docs/CARD_SYSTEM.md).
Wire utas_server to the store: /user/credits -> persisted coins,
/club -> persisted owned items, PUT /squad -> persists the squad the
user builds so it survives relaunches. Profile save file is gitignored.
Foundation for pack-opening (store/purchasegroup + transaction) next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o