c2e2e0d8f22f505fa99d61801ac35247bd908ef2
14 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
a3fd51692f |
fifa17-recon: the trading gate is still shut, and two of yesterday's conclusions were wrong
Ships the club-item subtype correction and the tradeable plumbing, and records two
refutations of claims made earlier in the same session. Nothing here is a working fix
for trading; the honest state is that the gate is still closed and we now know more
about why.
REFUTED 1: "the Blaze client-config store opens the trading gate". It does not, and the
flag is inert. IS_TRADING_ENABLED is an OUTPUT NAME. FUN_18006cc60 is a publisher: at
0x18006ccc6 it calls [rax+0x270] to READ gate byte 0x1fd2e, then lea rdx,[
IS_TRADING_ENABLED] and hands the value out under that name. The only rip-relative
reference to the literal 0x1801fc118 in all of .text is that lea; there is no comparison
against it anywhere, so no client-config key of that name can be read as an input. That
also undermines the IS_* store keys shipped beside it: their apparent success was never
actually attributed to them.
REFUTED 2: "the gate byte flipped to 1". It reads 0. It was measured as 1 shortly after
CardsDLL mapped and that was over-claimed as a success; a thorough re-measurement read 0
on the SAME pid and model pointer, and a fresh session reads 0 with an unambiguous raw
dump (model+0x1fd18.. = 01000000 00000000 00000000 00000000 3c000000 01 01 00 01, the 00
being 0x1fd2e). Either the first read was transient or something clears it after login.
The only writer is FUN_18011dc50 at 0x18011dc91, so a 0 means something RAN and wrote it.
AND THE "/settings IS DEAD" CLAIM FALLS TOO. FUN_18011dc50 is not unreachable: it is a
VIRTUAL method at model vtable slot +0x988 (absolute pointer 0x18021cc28). A direct-call
search found no callers because Ghidra does not resolve virtual calls, which is the same
dispatch-form trap that has now produced seven wrong verdicts here. The real chain is
settings response -> FUN_180174630 -> FUN_18013c6d0 (deser)
-> completion callback FUN_180173e00 -> vt+0x988 and vt+0x998 -> gate bytes
and FUN_180173e00 bails before applying anything unless the int at response+0x1c is
zero. Which atom writes +0x1c is unknown and is the thing worth chasing.
The measurement behind that claim also had a gap: it checked +0x1fd14, +0x1fd4c and
+0x1fd54 for the maximumTradePileSize=77 probe but NOT +0x1fd1c, which is the actual
TRADE_PILE_SIZE (read via vt+0xa58 = FUN_18011bf30). So the probe never tested the field
it needed to. Serving 77 and reading +0x1fd1c is the clean falsifier and is still open.
Recovered and worth keeping: an authoritative slot-to-name table from the publisher.
vt+0x270 IS_TRADING_ENABLED -> +0x1fd2e vt+0x2b0 IS_FRIENDLY_SEASON_ENABLED -> +0x1fd3a
vt+0x2b8 IS_TOURNAMENT_QUIT_ENABLED -> +0x1fd3b vt+0x2c0 IS_PROCESSING_STATE_ENABLED -> +0x1fd3c
vt+0x2c8 IS_DRAFT_MODE_ENABLED -> +0x1fd3d vt+0x2d8 IS_STORY_MODE_REWARD_ENABLED -> +0x1fd3f
vt+0x2f0 IS_RETURNING_USER_REWARDS_SCREEN -> +0x1fd40 vt+0xa58 TRADE_PILE_SIZE -> +0x1fd1c
That also locates the red TRANSFER LIST 0/0: it is +0x1fd1c, currently 0.
WHAT IS ACTUALLY SHIPPED HERE, all default off:
* FUT_TRADEABLE sends untradeable=false. Verified landing at item+0x49 (stored
INVERTED by case 0x361) on a live club record. Applied on every READ path, not only
in _item(), because the save holds 246 items minted before the flag existed and the
club route serves them straight from the save. That gap was caught by reading the
served JSON, not by unit-testing the factory.
* FUT_TRADING adds tradingEnabled and IS_TRADING_ENABLED to the Blaze config. Kept
only as a record of the refutation, with the reasoning inline so nobody retries it.
* fut_clubitems FAMILIES subtypes corrected: kit 9, stadium 10, badge 11 (cardtype 7,
not 9), ball 30, league logo 31. Every previous value sat in the 0x91..0x96 TROPHY
block. probe_shelf's candidate set lacked 9, 10 and 11, so the probe route the docs
preferred could never have answered this for three of five families.
* Club kits and badges now carry teamid, reintroduced ALONE after the 2026-08-05 crash
(which was never bisected; value is the established suspect and that response also
carried 30 items across five wrong subtypes). itemType dropped: it was unobserved and
never copied into the record.
Live: 439 contract checks, 414 card-family checks, market suite, all pass. The transfer
market still refuses with zero requests and the menu entries are still greyed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e578443d73 |
fifa17-recon: tradingEnabled is 0, and that is why the transfer options are greyed out
Card-subsystem pass, 11 agents plus three adversarial verifiers. Full writeup in docs/plan-2026-08-06-card-subsystem.md. Two of the results below correct things I committed earlier today. THE GREYED-OUT TRANSFER OPTIONS ARE EXPLAINED. "Place on Transfer List" and "List on Transfer Market" have been disabled in the reveal screen and nobody knew why. TO_TRADE_PILE (FUN_1801a7260) requires BOTH item+0x49 tradeable AND a service gate at vtable slot +0x270. That slot is `movzx eax, byte [rcx+0x1fd2e]; ret`, and 0x1fd2e is the tradingEnabled gate byte. Read live and reproduced independently: slot +0x2b0 friendlySeasons disp 0x1fd3a VALUE=1 slot +0x2c8 draftMode disp 0x1fd3d VALUE=1 slot +0x2e0 packOpeningAnim disp 0x1fd45 VALUE=1 slot +0x270 tradingEnabled disp 0x1fd2e VALUE=0 tradingEnabled is the FIRST gate byte found that is not 1. This partly rehabilitates the settings work from this morning: that plan died because every gate it targeted already read 1, and the conclusion drawn was that the settings array does not matter. It does. It matters for a flag nobody was looking at, and tradingEnabled is ALREADY in _SETTINGS_KEEP, plumbed and never sent because _SETTINGS_MODE defaults to off. So the fix is two things, not one: FUT_SETTINGS=keep AND untradeable false. Shipping only the boolean would look like the finding failed. THE DISCARD "MISS" NEVER EXISTED, which corrects |
||
|
|
e3092ca0f9 |
fifa17-recon: the client now shows the quick-sell value it is actually paid
Follow-on to
|
||
|
|
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>
|
||
|
|
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
|
||
|
|
d8ef9d4c4f |
fifa17-recon: the rareflag trap -- a rare Player Fitness card is a SQUAD Fitness card
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
|
||
|
|
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
|
||
|
|
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
|
||
|
|
0081dfc8d4 |
fifa17-recon: transfer market sell/list flow (stateful, tested)
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
|
||
|
|
89dc9baa61 |
fifa17-recon: fix active squad resetting on reload
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
|
||
|
|
5c43dbe39e |
fifa17-recon: pack opening (store catalog + buy + award)
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 |
||
|
|
bebe573d05 |
fifa17-recon: persistent FUT profile + starter pack
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 |