diff --git a/docs/FIFA17_TRANSFER_MARKET_WIRE.md b/docs/FIFA17_TRANSFER_MARKET_WIRE.md new file mode 100644 index 0000000..0c89598 --- /dev/null +++ b/docs/FIFA17_TRANSFER_MARKET_WIRE.md @@ -0,0 +1,149 @@ +# FIFA 17 Transfer Market — wire findings + +Reverse-engineering record for the FIFA 17 UTAS transfer-market surface, kept so +future agents do not reopen settled questions or re-guess enum spellings. + +Every claim carries a confidence tag: + +| Tag | Meaning | +|---|---| +| **CONFIRMED** | Observed from our own FIFA17.exe client or live host capture | +| **FIFA17-HISTORICAL** | Supported by contemporaneous FIFA 17 implementations (`lorenzh/fut-api`, `futapi/fut` v0.2.18 — the last pre-FIFA-18 release) | +| **INFERRED** | Best explanation, not directly captured | +| **UNKNOWN** | Requires instrumentation; do NOT implement from guesswork | + +Authority reminder: FIFA 17 field names, enum spellings, sentinel ids and +empty-state shapes come from captures or the Python oracle — never from a modern +FUT toolkit. Later-FIFA API drift is a known hazard, and reversing a container +type or inventing an enum is the documented client-freeze class. + +--- + +## The auction record (`auctionInfo[]`) + +What we emit today, on `/tradePile`, `/trade/status` and market browse: + +```json +{ + "tradeId": 1000000097, + "itemData": { "...full shaped card...": "", "itemState": "listFS" }, + "tradeState": "active", + "buyNowPrice": 15000, + "startingBid": 150, + "currentBid": 0, + "offers": 0, + "bidState": "none", + "expires": 3600, + "tradeOwner": true, + "sellerId": 33068179, + "sellerName": "CAGE", + "sellerEstablished": 1, + "watched": false, + "coinsProcessed": 0 +} +``` + +| Field | Confidence | Note | +|---|---|---| +| `tradeOwner` (bool) | **FIFA17-HISTORICAL** | Exists in FIFA 17 auctionInfo. That it is *the* Actions-panel gate is **UNKNOWN** pending live confirmation. | +| `sellerId` | **FIFA17-HISTORICAL** exists; type numeric is **INFERRED** | Set to the configured persona so it agrees with `tradeOwner`. Never baked in. | +| `sellerName` | **CONFIRMED** it must be the player | `fut_account.py` annotates the persona property as "Blaze PDTL.DSNM / LSX GetProfileResponse Persona / **UTAS sellerName**". EA's `"EASFC"` here is wrong for an own listing. | +| `offers` | **FIFA17-HISTORICAL** | `0` valid for active/unbid. | +| `bidState: "none"` | **FIFA17-HISTORICAL** | Valid for active/unbid. Other observed concepts: `highest`, `buyNow`. Do NOT "fix" this. | +| `expires` | **FIFA17-HISTORICAL** | **SECONDS REMAINING, not an epoch.** Historical durations: 3600, 10800, 21600, 43200, 86400, 259200. | +| `itemData.itemState: "listFS"` | **UNKNOWN** | Plausible and unchanged. Public FIFA 17 material gives no trustworthy enumeration. Do not guess replacements — capture. | +| `itemData.untradeable` | **FIFA17-HISTORICAL** field; our blanket `false` is **INFERRED** | See "Known debt" below. | +| `marketDataMinPrice` / `marketDataMaxPrice` | **do NOT add** | These entered the public parser only after its FIFA 18 migration. | + +### Why the differential could not catch the missing fields + +Our record's key set was **identical to the Python oracle's**, so field-for-field +parity was green. The oracle omits `tradeOwner` / `sellerId` / `offers` as well, +because *its* remove flow was never driven by a real client either — the only +historical live datapoint is a counts-tile bug. Oracle parity is therefore +**necessary but not sufficient** for any flow the oracle never actually served. + +The differential now asserts we cover every oracle key AND that our extra keys are +exactly `{offers, sellerId, tradeOwner}`, so the deliberate superset is pinned +while a new unexplained divergence still fails. + +--- + +## Routes + +| Route | Confidence | Behaviour | +|---|---|---| +| `GET …/trade/status` | **CONFIRMED** the client polls it continuously | Live auction-state refresh. It previously fell through `starts_with("trade")` into the buy/view arm, where the tail has no numeric id, so **every poll returned `{"auctionInfo": []}`**. Now a real handler: optional `tradeIds` filter, else the whole active pile. Unknown ids are absent, never an error. | +| `DELETE /ut/game//trade/` | **FIFA17-HISTORICAL** | The spelling contemporaneous FIFA 17 clients use, no body, no meaningful response body. Previously landed in the buy/view arm and **silently cancelled nothing while returning 200.** Now maps to MarketCancel. | +| `DELETE /ut/delete/game//trade/` | **CONFIRMED** (oracle) | The oracle's spelling; retained because the differential exercises it. Whether FIFA17.exe ever uses it is **UNKNOWN**. | +| `POST …/auctionhouse` | **CONFIRMED** | List for sale. The client sends only `itemData.id`; the server resolves wire id → Core instance → `card_id`/`resourceId` and enforces ownership. | +| `GET …/tradePile/counts` | **INFERRED** | Five scalar ints (`count`, `maxAuctionsAllowed`, `offered`, `selling`, `sold`); a DISTINCT deserializer from `/tradePile`. Exact FIFA 17 semantics of `count` (active auctions vs whole pile) is **UNKNOWN** — we report active auctions and deliberately did NOT speculate. | +| `PUT …/item` (move) | **FIFA17-HISTORICAL** | `{"itemData":[{"pile":"trade"|"club","id":ID}]}` → `{"itemData":[{id,pile,success}]}`. Transfer-List membership is a **separate operation from creating an auction**. | + +### Pile encoding + +* MOVE commands take a **string** pile (`"trade"`, `"club"`) — **FIFA17-HISTORICAL**. +* Returned `itemData.pile` is documented **numeric** in FIFA 17 auction data — **FIFA17-HISTORICAL**. +* The numeric mapping is **UNKNOWN**. Do not unify the two representations, and do + not derive a mapping from unrelated `pileSize` keys. + +--- + +## Q2 — Transfer List item that is not currently auctioned + +A real FUT state: an item in the Transfer List with no active auction (freshly +moved, or expired unsold). **CONFIRMED** to exist as a concept (the external hub +spec §27, and move-vs-list being separate operations). + +Its wire representation is **UNKNOWN**: `tradeId` 0 / omitted / null, `tradeState` +value or omission, and `itemData.itemState` are all unestablished. + +Consequence, and the reason this matters: our `/tradePile` renders only `active` +listings, so keying the `/club` exclusion on the `trade` **pile** stranded 4 cards +in no screen at all (hidden from the club, absent from the Transfer List). +Commit `f2c4927` keys exclusion on the **active listing** instead, which is +self-healing. That is a workaround, not fidelity — the faithful model needs the +unlisted state represented. + +**Required capture** (four states, full structural diff, not just a shortlist): + +``` +A. moved Club -> Transfer List, NEVER listed +B. actively listed +C. listing expired unsold +D. listing sold +``` + +Diff at least: `tradeId`, `tradeOwner`, `tradeState`, `bidState`, `expires`, +`offers`, `currentBid`, `startingBid`, `buyNowPrice`, `sellerId`, `sellerName`, +`itemData.id`, `itemData.itemState`, `itemData.pile`, `itemData.untradeable`. + +Do NOT drop the unlisted state from the model just because its encoding is unknown. + +--- + +## Deferred, with reasons + +* **5% transfer tax** — **INFERRED** architecture only: auction closes → Core + settles → seller credited gross × 0.95, with `auctionInfo` continuing to carry + gross. No trustworthy FIFA 17 field named `tax`/`netPrice`/`sellerProceeds` was + recovered, and no separate settle operation. Not blocking; do not couple + settlement to clearing the sold auction without a capture. +* **Bid / Transfer Targets** — not implemented. Watched / active bid / winning / + outbid / won / expired are distinct states and must not collapse to a flat list. +* **Unassigned** — FIFA 17 had a dedicated Unassigned service; the exact FIFA 17 + URL is **UNKNOWN**. Our 29-item `purchased` pile is this state and is currently + rendered inside `/club`. Do not manufacture a route from a modern toolkit. +* **Match CREATE / READY / PLAY** — **UNKNOWN** and explicitly not portable from + public FUT web-app work (the web app could not start matches). Instrument the + real client from Play Match to kickoff before implementing. + +--- + +## Known debt + +`shape_item` reports `untradeable: false` for **every** owned instance. Correct +today (Core models no untradeable items) and necessary — a hardcoded `true` greyed +out both list buttons — but it will misrepresent SBC / promo / loan rewards once +those exist. `untradeable` belongs on the owned-item instance as authoritative +state, not inferred from definition, resourceId or rarity.