413ad901fb2c3e30c9cf745ebf129538209a9141
19 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
aa5fb2cc40 |
test(market): make the sold A/B one-field attributable, add classified differential
The brief's gate: if the harness varies bidState AND coinsProcessed together, the client's reaction is attributable to neither. The env knobs were already orthogonal (--variant and --coins-processed are independent, cp defaults to 0), but sold-wire-check.py was flipping BOTH for variant B as a convenience, which is exactly the contaminated A/B the brief forbids. Fixed: the primary pair now holds coinsProcessed at 0 and asserts the differing-field set is exactly ['bidState']. New scripts/sold-ab-differential.py is the pre-live gate. It settles ONE synthetic sale, then re-reads every seller-facing surface under each variant by restarting only the host (same Core, same DBs, same sale), and diffs with explicit classification -- MISSING / EXTRA / TYPE_MISMATCH / VALUE_MISMATCH -- rather than a boolean "equal?". Two orthogonal pairs: PRIMARY bidState highest vs buyNow, coinsProcessed held at 0 ORTHOGONAL coinsProcessed 0 vs 1, bidState held at highest Result, 36/36: the ONLY finding on /tradePile is VALUE_MISMATCH auctionInfo[0].bidState A='highest' B='buyNow'; /trade/status differs in exactly the same one path; counts are byte-identical. The orthogonal pair's only finding is auctionInfo[0].coinsProcessed. C_cp0's sha256 equals A_highest's, so the capture is reproducible rather than merely consistent. Counts states the live run has to interpret, measured not guessed: S1 0 active + 1 sold -> count 0, selling 0, sold 1 S2 1 active + 1 sold -> count 1 (active mode) vs 2 (membership mode) That divergence IS the open question for the client; production is unchanged. scripts/sold-client-ports.py switches ONLY the two client Blaze port lines, and is built so restoration cannot depend on memory: it records the production values to a sidecar on the client BEFORE the first edit and restore reads that sidecar, refusing if it is absent. It rewrites only known keys (a missing key is an error, never a silent append), re-reads and verifies afterwards, and REFUSES to edit while a FIFA client is running because the hook reads the file at connect time. Phase 0 evidence under docs/evidence/sold-ab-2026-08-18/ with a sha256 per surface, one file per variant so A can never overwrite B. Live client A/B NOT run: a production FIFA session is currently live on 10.10.0.105 (pid 32188), and live-session mutual exclusion applies. The client config was NOT touched -- the switcher's guard refused, as designed. Production untouched: prod-host pid 3631953, coins 29,843,976, /tradePile 0, counts.sold 0, club 1966; nothing under /home/alex/openfut-promotion/state/ opened. |
||
|
|
571c5f9261 |
docs(market): recover the FIFA17 sold wire contract from CardsDLL (Ghidra)
Task A, static phase. Ghidra 12.1.2 headless via the repo's own pyghidra harness
over CardsDLL_Win64_retail.dll (13,382 functions). Queries and raw decompiler
output committed under docs/evidence/market-sold-re-2026-08-17/.
RECOVERED FROM THE BINARY
1. No sold token, now EXHAUSTIVELY: both vocabularies dumped to their sentinels
rather than sampled. tradeState is exactly 4 rows; itemState is exactly 12
(invalid/free/WAITING_FOR_GAME/inGame/forSale/offered/activeBadge/
activeHomeKit/activeAwayKit/activeBall/activeStadium/active=255). A sold row
MUST therefore be a combination of existing atoms.
2. What closed does, complete, from the auctionInfo deserializer 0x18013e410:
IS_GLOW = (tradeState==closed) ? bidState != none
: bidState in {outbid, buyNow}
INBOX = bidState in {highest, buyNow}
3. The full record -> Flash map from the publisher 0x1801bf030, superseding the
partial list. The prize: record +0xbf is published as COINS_AWARDED, fed by the
coinsProcessed atom 0x2f4. The corpus had recorded that atom's type and noted
its consumer was never found; it is now traced. DURATION also renders the
localised FUT_AUCTION_EXPIRED when expires underflows.
4. highest vs buyNow on a closed row is UNDECIDABLE from CardsDLL, by proof: both
yield IS_GLOW=1/INBOX=1, bit-identical. But bidState is ALSO published verbatim
as YOURBID alongside STATE and COINS_AWARDED, so the movie does receive the raw
values - the discrimination exists and lives entirely in unread ActionScript.
This retires the question as a static target, and it contradicts the
third-party lore that a seller's sold row is closed+buyNow (the corpus's own
lifecycle table says closed+highest and assigns buyNow to the buyer).
5. The clear-sold verb EXISTS. Builder 0x1801647c0 emits "/sold" when the tradeId
field is zero and "/%lld" otherwise, on route base ut/delete/%s/trade, response
class RS4 FutISRemoveTradeServerResponse. Confirmed by the client's own
request-name table entry RemoveAllSoldFromTradePile. A BULK clear-sold verb only
makes sense if sold rows PERSIST in the seller's pile until cleared, which is
incompatible with our Fix A invariant - so the sold path will require revisiting
it under live validation.
6. The seller's SOLD counter is real, proven end to end with no inference: the hub
tradePile sub-deserializer 0x18013ead0 writes atom sold 0x2c9 to +0x1d8, and the
tile publisher 0x1800b1dc0 renders +0x1d8 as Flash TEXT3 under the localised
caption FUT_TF_SOLD. Siblings: selling -> +0x1d2 -> FUT_TF_SELLING,
count -> +0x1d4 -> FUT_UC_ITEMS, plus FUT_TF_WINNING/FUT_TF_OUTBID on the
Transfer Targets tile. We and the Python oracle both hardcode sold:0, so that
bucket can never fill.
7. Reusable method: an atom id is the INDEX into the alphabetical atom-name pointer
table at base 0x1802d2760. Validated 12/12 against the known auctionInfo atoms
and cross-checked against fifa17-recon/docs/fut_atoms.tsv. Documented gotcha:
resolve a name by the pointer slot INSIDE the table, never by the first matching
string in the binary, or you get confident nonsense.
8. An auction-outcome vocabulary exists (auctionSoldBid 0x39, auctionSoldBuyNow
0x3a, auctionWon*/auctionLost*) but NO deserializer consumes it - every
candidate function was checked for the value-SKIP/atom-loop signature and none
qualifies. Server-side or telemetry only; it does not carry sold state here.
TASK B IS UNDECIDABLE FROM THE CLIENT, and this is a proof of absence: no 0.95 or
0.05 constant of either width, no tax/fee/net/proceeds caption, and no fee
arithmetic anywhere. The client never computes or displays a net, so no experiment
against our own server can measure the rounding - whatever we credit is what it
displays, and there is no oracle. Only an original EA-era seller-balance capture
could settle it. The rule stays an explicit CHOICE (floor the fee, so
fee + proceeds == gross exactly) and is now pinned at the requested boundaries
100/101/119/120/149/150/151/199/200 plus 15,000 and i64::MAX.
Settlement NOT promoted. No production process, port or database was touched.
|
||
|
|
cb32fe9b84 |
docs(market): close Gap 2 and freeze the transfer-list lifecycle as known-good
Return to Club is durable across a full FUT exit/re-entry — the last claim the forSale promotion could only make server-side. Same disposable card as Gap 1 (75 ST, res 212188, wire 100000178, tradeId 1000000178), after the 1h auction expired NATURALLY. No timestamp was mutated in either gap; a read-only sampler watched the whole hour (55 samples), because `expires` is derived from created_at + duration and watching is the only honest way to see expiry: active expires 3175 -> counting down -> expired expires 0, itemState forSale (absent) total 0 <- Return to Club itemState stayed forSale across active -> expired, the one state change Fix B had never been watched through live. The host log then shows the coupled transition and TWO session boundaries: route=move-items wire=100000178 pile=club auction_cancelled=1 route=auth-delete route=auth ... sid_opened=true (fresh session, x2) route=hub clubPlayers=1966 auctionCount=0 route=club total=1986 emitted=1966 auction_cancelled=1 is cancel_active_for_core_item firing, so pile membership and auction lifecycle cannot disagree. Two independent fresh sessions each rebuilt the state from durable storage and the operator confirmed the card was still in My Club; one boundary was the requirement. 18/18 server checks pass IDENTICALLY before and after re-entry: /tradePile total 0, counts all zero, tradeId -> closed with expires 0 (still resolves, correctly terminal), /club 1966 with itemState free, 0 duplicate ids, market store cancelled with 0 active and 0 reserved, coins 29,843,976 unchanged throughout. No code change was required for EITHER gap. Both tests existed to find out whether the promoted implementation was already correct on paths it had not been exercised on, and it was. Also freezes the full CLUB -> list -> active -> expired -> Return to Club -> CLUB lifecycle as the reference baseline, with the eight invariants it pins, so a future change that alters any line is a regression until proven otherwise. Explicitly NOT established: the SOLD path, /tradePile/counts semantics, the AVM1 gate. |
||
|
|
0a007f4941 |
docs(market): close Gap 1 — active seller row under forSale is live-confirmed
The one claim the Fix B promotion left open: no active listing existed during
that session, so only the expired path had been exercised.
Closed with a disposable card (75 ST, res 212188, wire id 100000178 — one of
three identical copies, not in the squad) listed through the real FIFA 17 client
at 150/200 for 1h, so expiry arrives naturally. No timestamp touched.
Wire: itemState=forSale, tradeState=active, 12 atoms, prices intact, expires
3562 -> 3556 over a 6s sample (live clock), /trade/status coherent, counts
{count:1, selling:1}, coins unchanged, zero inactive rows (Fix A intact).
The decisive evidence is client-side, not ours: a read-only /proc/<pid>/mem
decode of the live trade-pile auction record returned
itemState=5(forSale)
on the very field that read -1(<unrecognised>) under listFS. Direct A/B on the
only changed field, taken from the client's own memory.
Operator confirmed the row renders under LISTED ITEMS with correct prices, a
counting-down timer, normal art, and correctly non-actionable while active.
No code change required — the promoted implementation was already correct on the
active path. Claim boundary unchanged: this proves the client DECODES the token
and says nothing about the Flash action-gate term.
|
||
|
|
0c4aee6164 |
market: promote forSale — live-confirmed on the expired path
Operator drove the expired row 1000000155 (res 158023, 93 RW) in FIFA 17 with the candidate deployed: the row was still actionable, Return to Club was offered, and it worked — "the card is back in my club". Server-verified durable afterwards, which is what a fresh session reconstructs: /tradePile total 0 with zero rows, /tradePile/counts all zero, the card present in /club (1965 -> 1966 items, itemState "free"), zero duplicate ids, no stale active listing anywhere in the store (both rows cancelled), and the ended auction projecting as `closed` (4) on /trade/status. Fix A intact: zero `inactive` rows. So `listFS` -> `forSale` is protocol-correct AND behaviour-preserving on the path that matters, and `listFS` is gone from production serialization. Also worth recording what the result rules out: CARD_OFFERSTATE is NOT a gate term that requires -1. Every actionable row we had ever seen carried itemState -1, which looked like a possible client rule; it was a coincidence of our own invalid token. An expired row decoding CARD_OFFERSTATE = 5 stayed actionable. Deliberately NOT claimed: anything about the Flash action gate itself. STATE and the RESERVEDPRICE/MAX_CREDITS pair are untouched and still confounded, so the gate remains Category C / STRONGLY SUPPORTED / not proven, and AVM1 disassembly of tradepile.isInActiveAuction is still the separate next investigation. One gap left open honestly: no ACTIVE seller row existed during the session, so active-row rendering under `forSale` is unverified. /transfermarket has always emitted `forSale` on active rows, so it is expected-safe, but it has not been seen. |
||
|
|
a57f4930f0 |
market: emit FIFA 17's own forSale itemState, not the oracle's listFS
Single-field protocol-correctness fix, deployed as a candidate for a live A/B. `itemData.itemState: "listFS"` on the seller's own auction rows is not a FIFA 17 token at all: zero occurrences in `CardsDLL_Win64_retail.dll` (md5 4de3493131d7d2ff7f8b360c5ac9b655), zero in 4.26 GiB of live client memory, and it decodes to -1 through `FUN_180166660` — so the client was handed an unrecognised `CARD_OFFERSTATE`. FIFA 17's value for an item offered for sale is `forSale` (5), from the 12-row table at 0x180229cc0. Changed only where the invalid token was emitted: `handle_market_query` (GET …/tradePile) and `handle_market_status` (GET …/trade/status). The market search path already emitted `forSale` and is untouched — which is also why the risk here was lower than it looked: the client has been decoding `forSale` on a live route all along, and only the seller's own pile carried the bad value. Wire A/B on the same expired row: EXACTLY one field differs. tradeId, tradeState, expires, startingBid, buyNowPrice, currentBid, bidState, sellerName, sellerEstablished, watched, coinsProcessed, the twelve-atom count and the whole itemData card are byte-identical; coins unchanged at 29,843,976; Fix A's zero `inactive` rows intact. The differential asserted PARITY on this field and therefore passed while BOTH sides were wrong — the exact mechanism by which the defect survived every run. `market query tradePile` is now DIFFERENT-BY-DESIGN, pinning oracle == "listFS" and rust == "forSale" so the divergence cannot silently close again. Where the FIFA 17 binary contradicts the Python oracle, the binary wins. Gates: 126 host tests, 214 adapter tests, fmt clean, clippy clean. NOT claimed: that this preserves the list -> expire -> Return-to-Club lifecycle. That needs an operator FIFA 17 session and has NOT been observed yet. Also not claimed: anything about the Flash action gate — `CARD_OFFERSTATE` is one of three still-confounded candidates and this change does not test it. Revert is one line if the live test fails. |
||
|
|
f9ca901a50 |
market: stop advertising unlisted pile members as tradeState:"inactive"
RE of the FUT front-end closed the question the Actions-panel investigation left open, and the answer retracts Q2 rather than completing it. `tradeState` reaches exactly ONE native branch in CardsDLL — `cmp …,0x4` at `0x18013e619`, "is it closed?" — and `inactive`(2) and `expired`(3) take the same edge, producing bit-identical `flagA`/`flagB` (exhaustive 22-site census of `[reg+0x88]` reads across the PE; confirmed live, both classes read glow=0 inbox=0). The value is then handed to the movie verbatim as the Flash property `STATE`, and the action gate lives in the APT/ActionScript FUT front-end: the trade-pile class partitions rows with `getCardsInAuction`/`isInActiveAuction` (traces `initPile() - IN AUCTION:` / `- NOT IN AUCTION:`) and only auction rows reach `PreCheckCardOptions` -> `handleTradeCardAction`. A non-auction row renders and can never be acted on, which is exactly what the operator saw. So the rows were never usable. "LIVE-CONFIRMED" established that they RENDER, which is not the same claim, and I treated it as if it were. The corpus said this before any of it was built — `plan-2026-08-06-transfer-market.md:731-733`: "`inactive` decodes but no client path treats it specially; do not emit it." The earlier note explaining that the warning "was written about the PRESENTATION function" was motivated reasoning. This also fires the corpus's own pre-registered falsifier E3 (:368-373). Removed: the `inactive` projection from `GET …/tradePile` and `…/trade/status`, `UnlistedCandidate`, `resolve_unlisted_pile`, `unlisted_record`, `Server::resolve_trade_pile`, and the two helpers that existed only to feed them (`MarketStore::blocking_core_items`, `Fifa17IdentityResolver::wire_for_owned_id`). Unlisted trade-pile membership is now internal state with no wire expression. Nothing is stranded: `/club` excludes only items with an ACTIVE listing, so an unlisted pile member stays visible in the club, which is where the client can act on it. Verified live after deploy — `/tradePile` total 7 -> 1 with zero `inactive` rows, `/trade/status` resolving only the real auction, coins unchanged at 29,843,976, and all six former rows present in `/club` (1965 items). Tests: 126 pass, fmt + clippy clean. Two guards replace the three tests that pinned the old behaviour: `the_trade_pile_advertises_only_real_auctions` and `trade_status_answers_only_about_real_auctions`. NOT fixed here, deliberately: `itemData.itemState: "listFS"` is not a FIFA 17 token (0 occurrences in CardsDLL md5 4de3493131d7d2ff7f8b360c5ac9b655, 0 in 4.26 GiB of process memory, decodes to -1; the real value is `forSale` = 5, and the Python oracle emits `listFS` too — which is why the differential never caught it). `CARD_OFFERSTATE` is one of three unresolved action-gate candidates and every actionable row observed carried -1, so that change ships alone with its own live A/B. |
||
|
|
afadb13de4 |
feat(market): PHASE C — expose every unlisted trade-pile item as tradeState "inactive"
Q2 is LIVE-CONFIRMED (operator saw the inactive row under TRANSFER LIST with Start Price 0 and no Buy Now / Current Bid / timer, active rows still separate under LISTED ITEMS, and the state survived a full FUT exit/re-entry). Promoting from the bounded one-item probe to the real behaviour: the env gate is gone and /tradePile now enumerates the whole trade pile. Mechanism: read the pile (async), resolve each member to a shaped card (sync, because the identity/Core resolvers are not `Send`), then build the response (async). The core->wire lookup is `wire_for_owned_id`, which uses the identity store's NON-allocating `external_for` -- enumerating a pile is a READ and must never mint a wire id for an item the client has not seen. Items with no mapping, no Core record or no resolvable FIFA identity are skipped, never faked. Includes a bug the DIFFERENTIAL caught and unit tests did not: a pile row OUTLIVES its auction, so after a sale the seller's `trade` row is stale, and filtering only on ACTIVE listings re-advertised a SOLD card as an owned unlisted item. Suppression is now by listing state via `blocking_core_items()` -- active (real auction shown instead), reserved (sale in flight) and sold (card gone) -- while `cancelled` is deliberately NOT suppressed, because a cancelled listing means the card came back to the pile. New test covers all three plus the store-level rule. counts semantics deliberately unchanged: `count`/`selling` still track auctions only. 341 tests pass, 0 failed, clippy clean. Deployed: the 6 previously stranded pile items now render, alongside the 1 active listing, with Ronaldo correctly in /club and out of the pile. Body preserved as phase-c-full-pile-exposed.json. |
||
|
|
b6398c44e6 |
docs: record the LIVE-CONFIRMED inactive UI contract and the expired->Club transition
Operator confirmed in the real client that a tradeState "inactive" row lands under the right-hand TRANSFER LIST section and renders Start Price 0 with Buy Now, Current Bid and Time Remaining all absent, while an active row in the same body continued to render separately under LISTED ITEMS. That is the Q2 representation confirmed live, with the token itself dumped from the client's own string table rather than guessed. Records the observed wire->UI contract as a table plus a fixture (inactive-row-live-confirmed.json), and names the regression tests that pin it, including the two guards that the row is never emitted for an item outside the trade pile and never duplicates a real auction. Also records the expired->Club coupled transition verified server-side: the listing went to `cancelled`, the pile went to `club`, /tradePile dropped the item, /club regained it, and clubPlayers went 1964 -> 1965. That is durable store state rather than a client-local view, so it survives a session boundary by construction; tagged pending the operator's final exit/re-enter confirmation. Adds the counts observation table. `count` currently tracks AUCTION entries and not total Transfer List membership; semantics deliberately left unchanged until the full state set has been observed. No behaviour change in this commit. |
||
|
|
a2bd048ace |
feat(market): bounded Q2 candidate — one unlisted pile item as tradeState "inactive"
PHASE A settled the token from the CLIENT ITSELF, so this is not a guessed enum.
vocab_dump.py (new; static, read-only, VA->offset through the real PE section table)
dumps CardsDLL's NULL-terminated {const char*, int} vocabularies. The tradeState
table at 0x180229e40 reads exactly:
'active' = 1 'inactive' = 2 'expired' = 3 'closed' = 4
The sibling tables (type/zone/lev/pos) match the corpus verbatim, which validates the
dumper. So "inactive" is a token the client's own parser decodes.
PHASE B, bounded as instructed. `OPENFUT_FIFA17_UNLISTED_PROBE=<wire id>` exposes
EXACTLY ONE unlisted trade-pile item on /tradePile as a non-active record; unset,
behaviour is byte-identical to before. The other stranded pile items are untouched --
no bulk migration.
Why this shape is forced rather than chosen: the route table has exactly one
trade-pile route, it carries only twelve-atom auction records, `pile` (0x226) has no
deserializer arm so membership comes from the owning list, and of those atoms only
tradeState expresses lifecycle. The row carries tradeState "inactive" with
expires/prices/bid all zero so it cannot render a countdown or a price, and reuses the
item's stable tradeId because the client keys its record store on tradeId and
re-parents itemData -- so listing the item later UPDATES the row instead of leaving a
duplicate ghost.
Both preconditions are re-checked at response time: the item must actually be in the
`trade` pile, and it must not already own a listing. Two tests cover exactly those.
counts semantics deliberately unchanged -- the inactive row is not counted.
340 tests pass, 0 failed, clippy clean. Deployed; the wire now carries all three
lifecycle states at once (expired 1000000097, active 1000000155, inactive 1000000059)
and that body is preserved as a fixture.
|
||
|
|
2e97ff1461 |
docs+tools: dump the CardsDLL route table; narrow Q2 to one candidate by elimination
Re-entry discriminator came back a CONFIRMED BUG: an unlisted transfer-list item does not survive a fresh FUT session, so our representation cannot reconstruct trade-pile membership. Evidence acquisition per instruction, corpus and PE first, no guessing. Adds route_table_dump.py: static read-only dump of CardsDLL's route table from the on-disk PE, resolving VA->file offset through the real section table instead of assuming a single .text mapping. Output preserved as evidence. It settles "is /tradePile the only relevant route?" -- the table holds 45 routes plus 3 empty admin slots, and row 30 `ut/%s/tradePile` is the ONLY trade-pile route. There is no trade-pile items route. That plus three existing PE facts narrows the representation to exactly one candidate by ELIMINATION rather than choice: the route carries only twelve-atom auction records; `pile` (0x226) has no arm in the item deserializer so membership is conferred by the owning list and cannot be added as a field; of the twelve atoms only tradeState expresses lifecycle; and tradeState's closed vocabulary (active=1 inactive=2 expired=3 closed=4) has exactly one value not already spoken for. So an unlisted item can only be an auctionInfo record with tradeState "inactive". Tagged INFERRED-BY-ELIMINATION, not CONFIRMED: the remaining unknown is whether the Flash Transfer List RENDERS such a record in the unlisted section. Records the acceptance test (survive a full FUT reload) and the revised invariant that a transition is complete only when a fresh session reconstructs the same visible state. No behaviour change in this commit. |
||
|
|
7f37b37be3 |
docs: capture the unlisted transfer-list state and model the pile/auction boundary
Q2 measured, not guessed. The operator moved a card Club -> Transfer List without listing it (PUT /item, no POST /auctionhouse) and the state was captured read-only. Finding: we do not represent the unlisted state on the wire AT ALL. Such an item is byte-identical to a club item -- itemState `free`, no `pile` field emitted, still returned in /club and counted in clubPlayers -- while an actively-listed item is correctly excluded from /club and present in tradePile. Only the host's own pile store knows the difference. Trade pile held 6 items: 1 listed, 5 unlisted. The FIFA 17 ENCODING of that state stays UNKNOWN on purpose: returned itemData.pile is numeric with an unrecovered mapping, and tradeState is a closed table walk where an unrecognised bidState is silently swallowed as `none`, so a wrong enum produces a plausible-looking but wrong UI. The corpus warns `inactive` decodes but no client path treats it specially. One client-only discriminator is recorded instead. Also models the domain boundary both limbo bugs came from: pile membership and auction lifecycle are separate facts requiring coordinated transitions. States the testable invariant -- an item must never be simultaneously excluded from /club and absent from /tradePile -- with the two ways it was reachable and the commits that closed each. |
||
|
|
6cc22e5cc5 |
docs: freeze the known-good auction state and mark tradeOwner DISPROVEN
Records the live-client resolution so the market shape is now a fixture rather than folklore, and so a future agent cannot burn deployments on tradeOwner again. Confidence notes updated: tradeOwner remains FIFA17-HISTORICAL (it does exist in the FIFA 17-era API) but "required by FIFA17.exe Transfer List Actions" is now DISPROVEN for this client path -- it is not among the twelve atoms the client's auctionInfo deserializer reads, and it was implemented, deployed, observed inert and removed. The real blockers are recorded as CONFIRMED live-client findings: trade/status polling is load-bearing, `expires` must EVOLVE with wall-clock time (a frozen value is structurally valid and behaviourally broken), and relist must persist through the PK conflict that FIFA's re-sent ISStart necessarily causes. Freezes the known-good bodies under docs/evidence/market-lifecycle-2026-08-17/ with a machine-checkable countdown proof (_index.json._countdown_proof records expires decrementing, frozen:false) rather than asserting the clock in prose. States the general rule this cost us: a response can pass differential parity and render perfectly while still being wrong, because FIFA expects an evolving server-side state machine, not a static object that resembles one. |
||
|
|
e06fd57211 |
feat(host): migrate non-economy UTAS routes to Rust + launcher redesign
Host/adapter (deployed to prod-host):
- POST /ut/auth (+/ut/delete/auth): Rust mints sid, opens Rust session, adopts
persona from body; POST /openfut/account/sync full Rust envelope.
- GET /userMassInfo: full Rust (was proxy+overlay), shared build_user_mass_info.
- GET/PUT /clientdata/<key>: new clientdata_store.rs (JSON-persisted).
- GET /club/stats/{country,league,team}: context-aware club_stats_body
(nation/league/team buckets).
- GET /store,/match/keepalive,/captcha,/tfa,/livemessage,/activeMessage: StaticAck.
- GET /watchList, /squad/0, /user: Rust handlers.
- host_test.rs updated for the new routing.
Launcher: bump gitlink to
|
||
|
|
0bc71dbd74 |
docs(evidence): capture full-length UTAS responses + userMassInfo envelope
Fix extractor truncation (bound each HTTP message by Content-Length): userMassInfo (8 KB) and purchasegroup responses are now complete in the committed corpus, not cut at 4 KB. Document the userMassInfo envelope contract (target shape for a future full-Rust migration; needs the clubAbbr/established account triad Core lacks). |
||
|
|
2ecd830d75 |
docs(evidence): commit fresh sanitised UTAS wire captures (2026-08-15 live A/B)
Rebuilds the primary-capture corpus lost to .gitignore (Known Issues #200): 10 real-client requests + 12 responses captured during the post-P1 staging A/B on a real FIFA 17 client, sanitised (SID/authCode/deviceId/MAC/tokens redacted; raw pcap withheld). Documents the account/sync, empty-My-Packs 65534 sentinel, and userMassInfo contracts, incl. the finding that account/sync is coupled to Python active-profile selection (so it can't migrate standalone from the userMassInfo hybrid). |
||
|
|
fc29c2eb9b |
docs(fifa17): record no-sentinel client resolver proof
Land the client-side empty-My-Packs resolver evidence and the session-stability invariant established by the F3/R1 experiments. - PART III (F3, CONFOUNDED CRASH): a mid-process sentinel -> no-sentinel flip left a stale POSITIVE My-Packs ordinal that still took the resolve branch and crashed at 0x180014882. Preserved verbatim (not a guard failure). - PART IV (R1, SUCCESS): backend set no-sentinel first, then a FRESH FIFA process; genuine purchasegroup response ids [1,5,6,7] is byte-identical to the F3 capture, so client process lifetime is the only changed variable. Store opens on Browse Packs, no crash, no dialog. Guard PROVEN on the tested build. - New INVARIANT: empty-My-Packs capability MUST be session-stable -- the server must not switch a running client between sentinel-present and sentinel-absent for the My Packs group within one FIFA process, because the client caches the group ordinal and a stale positive ordinal still crashes the resolver. - Both no-sentinel captures kept: client_guard (F3) and freshretest (R1). Backend P2 active-sentinel (65534) remains production default; no capability handshake is implemented yet. |
||
|
|
b2697b13dc |
docs(fifa17): establish verified card taxonomy
Single source of truth for FIFA 17 FUT card families, reconciled against the authoritative shipped fcc_*.json + staff tables (verified byte-identical between .105 and this repo, 36/36 sha256). - docs/CARD_TAXONOMY.md: family -> table/rowcount/subtype/carddbid/cardassetid, with OBSERVED/INFERRED/HYPOTHESIS/UNKNOWN labels. Corrects four superseded claims (chem styles are 250-273 not 91-136; 6300/6400xxx are kits not badges; 5004xxx misc and 8010xxx league logos exist). Manager-league precision kept distinct: shipped table 300-340 (41 rows) vs client enum 300-341 (341 defined, unshipped). Club-item wire subtype->family mapping preserved as UNKNOWN. - docs/evidence/fifa17-recon/table-hashes.sha256: 36-file provenance manifest (31 fcc_*.json + 5 staff tables), combined hash 10f239ad... Describes the FIFA 17 data/client model only; not OpenFUT Core assumptions. |
||
|
|
e8ee6c34e7 |
docs(fifa17): record empty My Packs client contract
Full investigation record for bug 6c: baseline + Experiments A/B/C', Candidate F (contradicted), the explicit active-placeholder selection test, the minidump-confirmed CardsDLL crash, and the P2 decision. Marks ROOT CAUSE ESTABLISHED and documents the known UX limitations and the client-side follow-up. Files: docs/evidence/STORE_TILE_6C.md, docs/evidence/FIFA17_EMPTY_MYPACKS_CLIENT_CONTRACT.md, the four genuine /store/purchasegroup captures (baseline, mypacks70, empty_no_sentinel, active_placeholder), and docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md (client-side design/research). |