docs: remove docs/ — migrated into the OpenFUT-Vault (single source of truth)
The entire docs/ tree (24 top-level notes, 68 evidence captures, 2 plans, research) has been migrated into ~/OpenFUT-Vault, the curated Obsidian vault, which is now the sole home for project documentation. Merges preserved all detail (obsolete material kept under "Superseded" sections); evidence/plans were copied byte-identical; every migrated note records its Source: docs/<original>.md provenance. Vault commit f55a5ba. Documentation lives in the vault from here on. Code/script comments that still reference docs/ paths are stale pointers only (no build dependency); they can be repointed at the vault opportunistically. References to fifa17-recon/docs/ are a different tree and are unaffected.
This commit is contained in:
@@ -1,142 +0,0 @@
|
||||
# FIFA 17 FUT — Card Taxonomy (single source of truth)
|
||||
|
||||
Status: verified 2026-08-12. This document supersedes every earlier scattered
|
||||
taxonomy claim in the repo (see "Superseded taxonomy" at the bottom).
|
||||
|
||||
Evidence labels: **OBSERVED** = read directly from a shipped table or decompiled
|
||||
function; **INFERRED** = derived from OBSERVED facts; **HYPOTHESIS** = plausible,
|
||||
not yet proven. `UNKNOWN` is a valid answer and is stated as such.
|
||||
|
||||
## Provenance
|
||||
|
||||
- Authoritative tables: `10.10.0.105:/home/alex/Documents/OpenFUT/fifa17-recon/data/tables/`,
|
||||
copied byte-identical into this repo at `fifa17-recon/data/tables/`.
|
||||
SHA-256 manifest + verification: `docs/evidence/fifa17-recon/table-hashes.sha256`
|
||||
(36 files: 31 `fcc_*.json` + 5 staff tables; combined hash-of-hashes
|
||||
`10f239add919089354d8dbff873fc9737b0a0f80f6ac41b1aa2a096c0ec8d331`).
|
||||
- Each table file is a DECODED dump `{table, source, rowcount, schema[], rows[]}`;
|
||||
card instances are in `rows[]`, one object per card. Row counts and id ranges
|
||||
below were re-extracted from the in-repo `.120` copies.
|
||||
- Semantic (subtype→kind) labels are decompiler-derived, carried in
|
||||
`fifa17-recon/tools/fut_consumables.py` (`BY_SUBTYPE`, `_DOC`), generated by
|
||||
`tools/build_consumables.py` from `FUN_1800d8330`, `FUN_18013f4d0`,
|
||||
`FUN_1801bfac0`, `FUN_1801aa230`, `FUN_180048780` + the fcc tables.
|
||||
- Staff family selection: `fifa17-recon/docs/CARD_SYSTEM.md` (2026-08-04/05 blocks)
|
||||
and `fifa17-recon/tools/fut_staff.py` / `fut_coaches.py`.
|
||||
|
||||
## Family selector (OBSERVED, binary)
|
||||
|
||||
Before registering an item, `FUN_180141660` merges the client's local DB. It
|
||||
switches on record `+0x4c` (`cardtype`), which `FUN_1800d8330` derives from JSON
|
||||
atom `0x6c cardsubtypeid` alone:
|
||||
|
||||
```
|
||||
cardsubtypeid 0..3 -> cardtype 1 players
|
||||
4 -> cardtype 2 managercards
|
||||
5 -> cardtype 3 headcoachcards
|
||||
6 -> cardtype 10 gkcoachcards
|
||||
7 -> cardtype 5 physiocards
|
||||
8 -> cardtype 4 fitnesscoachcards
|
||||
0x1e,0x1f,0x91..0x96,0xe7..0xe9,0xec -> cardtype 9 club items + misc
|
||||
absent (default 342) -> cardtype 0 no merge
|
||||
```
|
||||
|
||||
Key: `carddbid == resourceId` for non-player families (queried RAW, no mask);
|
||||
players are queried by `playerid = resourceId & 0xffffff` (atom `0x287`), and
|
||||
`assetId` (atom `0x23`) is never read by the merge.
|
||||
|
||||
## Consumable & staff families — evidence table (OBSERVED, `.120` tables)
|
||||
|
||||
| Family | Table | Rows | carddbid range | cardsubtype set | cardassetid (ART) |
|
||||
|---|---|---|---|---|---|
|
||||
| Contracts | `fcc_contractcards.json` | 13 | 5001001–5001013 | {201, 202} | {7, 8} |
|
||||
| Fitness + Healing | `fcc_healingcards.json` | 27 | 5002001–5002030 | {211,212,213,215,216,217,218,219,220} | {9, 10} |
|
||||
| Training (6 sub-families) | `fcc_trainingcards.json` | 143 | 5003001–5003159 | 51-57, 61-67, 91-110, 121-136, 250-273, 300-340 | {1,3,32,34,35,50,51} |
|
||||
| Misc | `fcc_misccards.json` | 42 | 5004001–5004042 | {231, 232, 233, 236} | {43, 44, 45, 46} |
|
||||
| Badges | `fcc_badgecards.json` | 656 | 6000000–6000656 | (none in row) | {39} |
|
||||
| Stadiums | `fcc_stadium.json` | 78 | 6200000–6200077 | (none in row) | {36} |
|
||||
| Kits | `fcc_kitcards.json` | 1482 | 6300000–6400654 | (none in row) | {35} |
|
||||
| League logos | `fcc_leaguelogos.json` | 44 | 8010000–8010044 | (none in row) | {40} |
|
||||
| League logo stickers | `fcc_leaguelogostickers.json` | 39 | 8010000–8010039 | (none in row) | {40} |
|
||||
| Balls | `fcc_balls.json` | 42 | 8120194–8120236 | (none in row) | {37} |
|
||||
| Managers | `managercards.json` | 417 | 1000001–1001552 | (staff; by cardsubtypeid 4) | (assetid col) |
|
||||
| Head coaches | `headcoachcards.json` | 124 | 2000004–2000328 | (staff; subtype 5) | — |
|
||||
| GK coaches | `gkcoachcards.json` | 121 | 9000001–9000324 | (staff; subtype 6) | — |
|
||||
| Physios | `physiocards.json` | 51 | 4000002–4000259 | (staff; subtype 7) | — |
|
||||
| Fitness coaches | `fitnesscoachcards.json` | 115 | 3000019–3000328 | (staff; subtype 8) | — |
|
||||
|
||||
Notes:
|
||||
- **Contracts (5001xxx)** — 201 = player_contract, 202 = manager_contract
|
||||
(OBSERVED, `BY_SUBTYPE`).
|
||||
- **Fitness + Healing (5002xxx)** — subtype **214 is a valid enum value but ships
|
||||
ZERO rows** (OBSERVED: 214 absent from `rows[]`). Enum split (OBSERVED,
|
||||
`BY_SUBTYPE`): healing = 211-218, player_fitness = 219, squad_fitness = 220.
|
||||
The `+0x58 rareflag == 1` trap flips subtype 219 to squad-fitness.
|
||||
- **Training (5003xxx)** — SIX sub-families, all OBSERVED in `rows[]` and labelled
|
||||
in `BY_SUBTYPE`:
|
||||
- `gk_training` 51-57 (7)
|
||||
- `player_training` 61-67 (7)
|
||||
- `position_mod` 91-110 (20)
|
||||
- `formation_mod` 121-136 (16)
|
||||
- chem/play styles 250-273 (24): `player_playstyle` 250-268 (19) +
|
||||
`gk_playstyle` 269-273 (5)
|
||||
- `manager_league` 300-**341** in the client enum (42), but the shipped table
|
||||
contains only 300-340 (41 rows) — 341 is defined, not shipped.
|
||||
- Client enum also defines vestigial/unshipped ranges NOT in the table:
|
||||
`manager_formation_mod` 71-86, and `DEAD_ZONE`
|
||||
58-60,68-70,87-90,111-120,203-210 (28). These have no rows.
|
||||
- **Misc (5004xxx)** — subtypes {231,232,233,236}; cardassetid ART 43-46.
|
||||
cardsubtype 231 = 0xe7 anchors the 0xe7..0xe9 block to misc via `FUN_1800d8330`.
|
||||
- **Club items (badges/stadiums/kits/logos/balls)** carry no `cardsubtype` in the
|
||||
row; they are keyed by `carddbid` range and distinguished on the wire by ART
|
||||
`cardassetid` (badges 39, stadium 36, kits 35, logos 40, balls 37). **Kits live
|
||||
at 6300000–6400654 and are NOT badges** (badges are 6000xxx). The client-side
|
||||
subtype→family map (which of 0x1e,0x1f,0x91..0x96 is ball vs stadium vs badge vs
|
||||
kit) is **UNKNOWN** — it is in none of the dumped tables and cardtype 9 has no
|
||||
miss-fill arm (see `CARD_SYSTEM.md`, "Club items", 2026-08-05).
|
||||
|
||||
## Staff — subtype/cardtype selectors (OBSERVED, binary)
|
||||
|
||||
| cardsubtypeid | cardtype | Family | Table | Rows | carddbid range |
|
||||
|---|---|---|---|---|---|
|
||||
| 4 | 2 | Manager | `managercards.json` | 417 | 1000001–1001552 |
|
||||
| 5 | 3 | Head coach | `headcoachcards.json` | 124 | 2000004–2000328 |
|
||||
| 6 | 10 | GK coach | `gkcoachcards.json` | 121 | 9000001–9000324 |
|
||||
| 7 | 5 | Physio | `physiocards.json` | 51 | 4000002–4000259 |
|
||||
| 8 | 4 | Fitness coach | `fitnesscoachcards.json` | 115 | 3000019–3000328 |
|
||||
|
||||
`cardsubtypeid -> rec+0x50` is the only family selector; `FUN_1800d8330` maps it to
|
||||
`rec+0x4c cardtype`. Manager merge = `FUN_1801356c0` (queries `managercards` by
|
||||
`carddbid` raw); coach merges live in the respective tables (`fut_coaches.py`).
|
||||
All five render live with real photos/bonuses, zero "DB Error" (CARD_SYSTEM.md,
|
||||
2026-08-05).
|
||||
|
||||
## Players (context, out of taxonomy scope here)
|
||||
|
||||
Players use cardsubtypeid 0..3 -> cardtype 1; merged by `playerid = resourceId &
|
||||
0xffffff` against the local `players` table. Identity/name/nation/team come from
|
||||
the client DB; rating/position/attributes come from our item JSON. See
|
||||
`CARD_SYSTEM.md` and the migration work in `openfut-adapter-fifa17`.
|
||||
|
||||
## Superseded taxonomy (report only — no other files edited)
|
||||
|
||||
The earlier working taxonomy (in prior agent-session notes / long-term memory, and
|
||||
partially in the original `TablesTaxonomy` scout output) got four things wrong.
|
||||
Corrected here:
|
||||
|
||||
1. **Chemistry / play styles are 250-273, NOT 91-136.** 91-110 = `position_mod`,
|
||||
121-136 = `formation_mod`. (OBSERVED in `fcc_trainingcards` + `BY_SUBTYPE`.)
|
||||
2. **6300xxx/6400xxx are KITS, not badges.** Badges are 6000xxx. (OBSERVED.)
|
||||
3. **5004xxx misc cards exist** (subtypes 231/232/233/236, ART 43-46). Previously
|
||||
omitted. (OBSERVED, `fcc_misccards`.)
|
||||
4. **8010xxx league logos exist** (+ stickers), ART 40. Previously omitted.
|
||||
(OBSERVED, `fcc_leaguelogos`.)
|
||||
|
||||
**Repo blast-radius of the superseded values: NONE.** A repo-wide grep
|
||||
(`docs/`, `openfut-adapter-fifa17/`, `openfut-core/`, `fifa17-recon/`) for the wrong
|
||||
claims (`91-136` chem styles; `6300/6400xxx` as badges) returns zero hits.
|
||||
`fifa17-recon/docs/CARD_SYSTEM.md` and `plan-2026-08-06-card-subsystem.md` already
|
||||
use the correct `250..273` for chem styles and the correct staff subtypes 4-8;
|
||||
`CARD_SYSTEM.md` is a mechanism log, not a taxonomy, and asserts none of the wrong
|
||||
ranges. The superseded values therefore live only in non-repo agent memory, which
|
||||
should be corrected to point at this document.
|
||||
@@ -1,175 +0,0 @@
|
||||
# openfut-core — Correctness Issues (audit 2026-08-17)
|
||||
|
||||
Read-only audit of `openfut-core` (game-agnostic axum + SQLite/sqlx economy authority).
|
||||
Four reported issue classes confirmed with exact `file:line` evidence, **plus a bonus
|
||||
HIGH-severity SBC duplicate-card economy exploit**. Nothing here is fixed yet —
|
||||
fixing bumps Core off the frozen P1 reference (`fbb54ea`, the current known-good
|
||||
production Core) and one item needs a DB migration + prod backfill, so this needs a
|
||||
**go/no-go** before rebuild+redeploy.
|
||||
|
||||
> **Zero live users right now**, so the HIGH-severity economy exploits are not
|
||||
> currently exploitable — but they are the exact class (coin overspend + card
|
||||
> duplication) that crashed live clients earlier (project memory), so they should be
|
||||
> fixed before any real play.
|
||||
|
||||
## Architecture context (why the bugs cluster)
|
||||
|
||||
Two generations of economy code coexist:
|
||||
- **NEW** `services/economy.rs` + `routes/economy.rs` — **fully atomic + validated**:
|
||||
every compound op acquires a connection, `BEGIN IMMEDIATE`, composes
|
||||
transaction-scoped primitives (`debit`/`credit`/`add_item`/`remove_item`/
|
||||
`consume_entitlement`), commits/rolls back via `finish()`, rejects negative amounts,
|
||||
and has an extensive in-module test suite. **This is the reference fix pattern.**
|
||||
- **OLD** per-feature services (`club`, `pack`, `market`, `sbc`, `checkin`, `upgrades`,
|
||||
`match_service`, `season`) — predate it, still do read/check/write directly against
|
||||
the `&Pool` with each statement on its own connection: no transaction. `economy.rs`'s
|
||||
own module doc explicitly warns these "cannot offer that guarantee".
|
||||
|
||||
Issues 2 and 3 live entirely in the OLD generation; the fix is to route them through
|
||||
`economy.rs`'s proven atomic ops (or give the pool helpers `&mut SqliteConnection`
|
||||
transactional variants). Request flow is `X-OpenFUT-Game` header → active profile →
|
||||
club → service; clients never pass a `club_id`, so cross-club access is **not** a
|
||||
vector — the risks are intra-club concurrency + unvalidated payloads.
|
||||
|
||||
---
|
||||
|
||||
## Issue 1 — `sbc_submissions` missing `club_id` (milestone always 0) · **LOW**
|
||||
|
||||
- **Root cause:** `migrations/0001_initial.sql:96-102` creates `sbc_submissions(id,
|
||||
profile_id, sbc_id, submitted_card_ids, passed, submitted_at)` — no `club_id`, and no
|
||||
later migration adds one. But `src/routes/club.rs:94-99` (`get_milestones`) runs
|
||||
`SELECT COUNT(*) FROM sbc_submissions WHERE club_id = ? AND passed = 1`. SQLite errors
|
||||
`no such column: club_id`; the error is swallowed by `.unwrap_or(0)` → the
|
||||
`sbcs_completed` milestone is **always 0**. Writer `services/sbc.rs:74-83` inserts
|
||||
`profile_id`, not `club_id`.
|
||||
- **Impact:** wrong milestone stat only. No crash, no economy corruption.
|
||||
- **Fix (matches the ticket — needs migration + backfill):** new migration `0019`:
|
||||
`ALTER TABLE sbc_submissions ADD COLUMN club_id TEXT;` then backfill
|
||||
`UPDATE sbc_submissions SET club_id = (SELECT c.id FROM clubs c WHERE c.profile_id =
|
||||
sbc_submissions.profile_id);` and bind `club_id` in `sbc.rs:submit_sbc`'s INSERT
|
||||
(`club_id` is already a param at `sbc.rs:41`).
|
||||
- **Simpler alternative (no migration):** change the `club.rs:95` query to
|
||||
`WHERE profile_id = ?` (column already exists). Ticket asks for the column, so both
|
||||
are recorded.
|
||||
- **Migration required:** YES (for the ticket's fix); NO (for the alternative).
|
||||
|
||||
## Issue 2 — Non-atomic check-then-act economy mutations (TOCTOU) · **HIGH**
|
||||
|
||||
Each does read → check → write across multiple pool round-trips with no
|
||||
`BEGIN IMMEDIATE`, so concurrent requests race → overspend / duplication / double reward:
|
||||
|
||||
| Site | Race |
|
||||
|---|---|
|
||||
| `services/club.rs:88-115` `spend_coins` | SELECT coins → `if balance<amount` → UPDATE; two concurrent spends both pass → **overspend / negative balance**. Shared primitive used by all callers below. |
|
||||
| `services/pack.rs:58-141` `open_pack` | read `pack.opened` → INSERT cards loop → UPDATE opened=1; concurrent double-open → **card duplication** |
|
||||
| `services/pack.rs:144-160` `buy_pack` | `spend_coins` then `grant_pack`, separate ops → crash between = coins gone, no pack |
|
||||
| `services/market.rs:129-186` `buy_listing` | SELECT sold=0 → spend → UPDATE sold=1 → INSERT; concurrent double-buy → **two cards minted** |
|
||||
| `services/market.rs:188+` `sell_card` | SELECT owned → DELETE → add_coins; concurrent double-sell → **double credit** |
|
||||
| `services/sbc.rs:35-107` `submit_sbc` | validate → DELETE cards → INSERT submission → add_coins/grant_pack; same cards to two SBCs → **double reward** |
|
||||
| `services/checkin.rs:60-120` `claim` | SELECT last → same-day check → reward → INSERT; concurrent → **double claim** |
|
||||
| `services/upgrades.rs:64-95` `change_position` | fetch → spend_coins → UPDATE |
|
||||
| `services/match_service.rs:97-235` + `season.rs` reward | many sequential writes; partial failure leaves partial rewards |
|
||||
|
||||
- **Impact:** corrupts economy state (coin overspend + card duplication).
|
||||
- **Fix:** wrap each compound op in one `BEGIN IMMEDIATE`…`finish()` transaction
|
||||
exactly as `services/economy.rs:214-236` already does; route pack/market/sbc/checkin/
|
||||
upgrades through `economy.rs`'s composed atomic ops (or add `&mut SqliteConnection`
|
||||
variants of `spend_coins`/`add_coins`/`grant_pack`/`add_item`).
|
||||
- **Migration required:** NO (code-only).
|
||||
|
||||
## Issue 3 — Missing input validation: SBC duplicate-card exploit · **HIGH**
|
||||
|
||||
- **Root cause:** `services/sbc.rs:53-70` iterates `req.owned_card_ids` with **no dedup
|
||||
and no length bound**. The same `owned_card_id` repeated N times resolves the same card
|
||||
N times (each `fetch_optional` succeeds); `validate_sbc` (`sbc.rs:110-116`) counts it
|
||||
toward `squad_size` and passes; the DELETE loop deletes it once → **a user satisfies
|
||||
any SBC with ONE card duplicated → free rewards**. Entry point `routes/sbc.rs:30-49`
|
||||
forwards `req` unvalidated. Unbounded Vec length is also a DoS.
|
||||
- **Fix:** in `submit_sbc`, reject duplicate ids (collect into a `HashSet`, compare
|
||||
`len`) and bound the list (e.g. ≤ 30) before resolving → `AppError::BadRequest`.
|
||||
- **Already-good validation (no change):** `economy.rs:64-67,84-87` reject negative
|
||||
amounts; `match_service.rs:100-104` clamps goals 0..99; `upgrades.rs` validates
|
||||
boost 1..3 / positions. Minor: `routes/economy.rs post_grant_reward` forwards an
|
||||
unbounded `amount` (trusted host caller; add an upper-bound sanity guard).
|
||||
- **Migration required:** NO.
|
||||
|
||||
## Issue 4 — `season.rs` panics + checkin index panic · **LOW**
|
||||
|
||||
- **Root cause:** `services/season.rs` `.expect()` on `fetch_optional` Options at
|
||||
`:23` (`get_or_create`), `:69` and `:144` (`record_match`) — panic if the `seasons`
|
||||
row is absent when expected. `seasons.profile_id` is PRIMARY KEY
|
||||
(`migrations/0006_seasons_loans_packs.sql:2`), so the concurrent-insert case surfaces
|
||||
as a UNIQUE error via `?` (not the panic), lowering probability — but it still
|
||||
panics-on-invariant, aborting that request (axum → 500 for the request; not a full
|
||||
server crash).
|
||||
- **Secondary:** `services/checkin.rs:~52,~88` index `STREAK_COINS[idx]` with
|
||||
`idx = ((streak-1)%7) as usize`; Rust `%` can be negative → a corrupt/negative
|
||||
persisted `streak_day` yields a negative index → **panic**.
|
||||
- **Fix:** replace each `.expect(...)` with
|
||||
`.ok_or_else(|| AppError::Internal("season row missing".into()))?`; guard the checkin
|
||||
index with `.rem_euclid(7)` (or clamp `streak_day >= 1` on read).
|
||||
- **Migration required:** NO.
|
||||
|
||||
---
|
||||
|
||||
## Fix plan summary
|
||||
|
||||
| Issue | Severity | Migration | Files |
|
||||
|---|---|---|---|
|
||||
| 1 sbc_submissions club_id | LOW | YES (or none via alt) | `migrations/0001` (+new 0019), `routes/club.rs`, `services/sbc.rs` |
|
||||
| 2 non-atomic mutations | HIGH | NO | `services/{club,pack,market,sbc,checkin,upgrades,match_service,season}.rs` → route through `services/economy.rs` |
|
||||
| 3 SBC duplicate-card exploit | HIGH | NO | `services/sbc.rs`, `routes/sbc.rs` |
|
||||
| 4 season/checkin panics | LOW | NO | `services/season.rs`, `services/checkin.rs` |
|
||||
|
||||
**Recommended order:** 3 (smallest, highest-value: kills the free-reward exploit) → 2
|
||||
(the transactional refactor, largest) → 4 (defensive hygiene) → 1 (cosmetic; do with
|
||||
the alt query unless the column is wanted).
|
||||
|
||||
**Deployment note:** all of these change `openfut-core`, which is currently frozen at
|
||||
the P1 reference (`fbb54ea`) in production. Fixing + rebuilding + redeploying prod-core
|
||||
is a deliberate step off that reference — get a go/no-go first. Only Issue 1's
|
||||
column-add needs a migration + prod backfill; 2/3/4 are code-only.
|
||||
|
||||
---
|
||||
|
||||
## Resolution (2026-08-17, on `openfut-core` @ `fbb54ea` + these edits)
|
||||
|
||||
All four issue classes fixed in the canonical superproject submodule
|
||||
`openfut-core`; full test suite green (179 tests) + clippy clean + a new
|
||||
regression test `tests/integration_test.rs::test_sbc_rejects_duplicate_cards`.
|
||||
|
||||
| Issue | Fix | Files |
|
||||
|---|---|---|
|
||||
| 3 SBC dup-card exploit | dedup (`HashSet`) + `MAX_SBC_CARDS`=30 bound in `submit_sbc`, before card resolution → `BadRequest` | `services/sbc.rs` |
|
||||
| 1 sbc_submissions club_id | migration `0019` adds `club_id` + backfills from `clubs`; `submit_sbc` INSERT now binds `club_id` | `migrations/0019_*.sql`, `services/sbc.rs` |
|
||||
| 4 season/checkin panics | `.expect()` → `.ok_or_else(AppError::Internal)?` (3 sites); checkin index `% 7` → `.rem_euclid(7)` (2 sites) | `services/season.rs`, `services/checkin.rs` |
|
||||
| 2 non-atomic mutations | statement-level compare-and-swap (see below) | `services/{club,pack,market,checkin}.rs` |
|
||||
|
||||
### Issue 2 — how it was fixed, and the residual
|
||||
|
||||
Rather than the full transaction refactor (threading `&mut SqliteConnection`
|
||||
through every service), the concurrency-exploitable races were closed with
|
||||
single-statement **compare-and-swap** — the atomic unit SQLite already gives us,
|
||||
no transaction plumbing, minimal blast radius on the working prod economy path:
|
||||
|
||||
- `club::spend_coins` — `UPDATE … SET coins = coins - ? WHERE id = ? AND coins >= ?`
|
||||
+ `rows_affected` guard; also rejects negative amounts. Kills **overspend** for
|
||||
every caller (the shared root primitive).
|
||||
- `pack::open_pack` — claims the pack (`UPDATE … opened = 1 WHERE … AND opened = 0`)
|
||||
**before** minting cards; loser aborts. Kills **card duplication** via double-open.
|
||||
- `market::buy_listing` — claims the listing (`sold 0→1`) before charging; releases
|
||||
the claim if the debit fails. Kills **double-mint**.
|
||||
- `market::sell_card` — `DELETE … WHERE id = ? AND club_id = ?` + `rows_affected`
|
||||
guard before crediting. Kills **double-credit** via double-sell.
|
||||
- `checkin::claim` — conditional `INSERT … SELECT … WHERE NOT EXISTS (today's row)`
|
||||
+ `rows_affected` guard; pays out only if the claim landed. Kills **double-claim**.
|
||||
|
||||
**Residual (accepted, documented):** the *multi-statement all-or-nothing* edges that
|
||||
need a real transaction to close — `pack::buy_pack` (spend then grant: a crash between
|
||||
loses coins with no pack), `sbc::submit_sbc` (concurrent submits sharing cards could
|
||||
double-consume mid-loop), and `match_service`/`season` reward chains (partial writes on
|
||||
crash). These are **partial-failure durability edges, not statement-level races**, and
|
||||
require concurrency that a single-player FIFA17 client does not generate. Closing them
|
||||
is the `&mut SqliteConnection` transaction refactor originally proposed; deferred as
|
||||
low-value for single-player. Overspend + duplication + double-credit — the vectors that
|
||||
corrupt economy state — are all closed.
|
||||
@@ -1,79 +0,0 @@
|
||||
# FIFA17 Content Completeness (33-record gap)
|
||||
|
||||
Evidence: ContentGapMap scout vs import-input.json (persona 33068179, 1995 items),
|
||||
fifa17-recon/data/tables, manifest/fifa17-import-manifest.json (OBSERVED).
|
||||
|
||||
## Summary
|
||||
| Category | Expected | Resolvable | Unrecoverable |
|
||||
|---|---|---|---|
|
||||
| Legend players | 13 instances (10 defs) | 0 | 13 (need .105 Legends locale names) |
|
||||
| Consumables | 17 | 17 | 0 |
|
||||
| Staff | 3 | 3 | 0 |
|
||||
|
||||
## Consumables (17/17 RESOLVABLE — carddbid present in fcc tables; semantics from cardsubtypeid)
|
||||
| wire_id | resourceId | subtype | kind | table |
|
||||
|---|---|---|---|---|
|
||||
| 100000239 | 5003012 | 54 | gk_training | fcc_trainingcards |
|
||||
| 100000249 | 5003011 | 54 | gk_training | fcc_trainingcards |
|
||||
| 100000260 | 5003004 | 52 | gk_training | fcc_trainingcards |
|
||||
| 100000272 | 5003059 | 91 | position/playstyle mod | fcc_trainingcards |
|
||||
| 100000250 | 5003060 | 92 | position/playstyle mod | fcc_trainingcards |
|
||||
| 100000327 | 5003065 | 97 | position/playstyle mod | fcc_trainingcards |
|
||||
| 100000238 | 5003066 | 98 | position/playstyle mod | fcc_trainingcards |
|
||||
| 100000261 | 5003068 | 100 | position/playstyle mod | fcc_trainingcards |
|
||||
| 100000316 | 5003068 | 100 | position/playstyle mod | fcc_trainingcards |
|
||||
| 100000293 | 5003103 | 258 | player_playstyle | fcc_trainingcards |
|
||||
| 100000326 | 5003112 | 267 | player_playstyle | fcc_trainingcards |
|
||||
| 100000283 | 5003116 | 271 | gk_playstyle | fcc_trainingcards |
|
||||
| 100000294 | 5001004 | 201 | player_contract | fcc_contractcards |
|
||||
| 100000304 | 5001008 | 202 | manager_contract | fcc_contractcards |
|
||||
| 100000305 | 5001009 | 202 | manager_contract | fcc_contractcards |
|
||||
| 100000315 | 5002027 | 217 | healing | fcc_healingcards |
|
||||
| 100000426 | 5002013 | 213 | healing | fcc_healingcards |
|
||||
|
||||
## Staff (3/3 RESOLVABLE)
|
||||
| wire_id | resourceId | subtype | role | table |
|
||||
|---|---|---|---|---|
|
||||
| 100000271 | 3000083 | 8 | fitnesscoach | fitnesscoachcards |
|
||||
| 100000427 | 3000083 | 8 | fitnesscoach | fitnesscoachcards |
|
||||
| 100000282 | 9000081 | 6 | gkcoach | gkcoachcards |
|
||||
|
||||
## Legends (13 instances / 10 defs — UNRECOVERABLE from .120)
|
||||
All defer via NoName gate (openfut-import-fifa17/src/lib.rs:476-478). 6 assets absent
|
||||
from players.json; 236250/236253/236257 resolve only to placeholder nameid 24313='171918'.
|
||||
dcplayernames.json/editedplayernames.json EMPTY in .120 dump. Version formula holds for all
|
||||
(resourceId == (version<<24)|assetId) — so they auto-promote the instant a .105-derived
|
||||
name row exists; NO code change needed, only name data.
|
||||
|
||||
| asset | resourceId(ver) | rareflag/rating/pos | wire copies |
|
||||
|---|---|---|---|
|
||||
| 169193 | 169193(v0) | 1/r87/CDM | 100000057,100000083,100000146,100000157 |
|
||||
| 211029 | 211029(v0) | 1/r73/CB | 100000165 |
|
||||
| 224512 | 224512(v0) | 1/r67/LB | 100000170 |
|
||||
| 227403 | 227403(v0) | 1/r64/ST | 100000160 |
|
||||
| 236743 | 236743(v0) | 1/r60/LB | 100000163 |
|
||||
| 237510 | 237510(v0) | 1/r59/CB | 100000162 |
|
||||
| 236250 | 17013466(v1) | 3/r78/ST | 100001102 |
|
||||
| 236253 | 17013469(v1) | 3/r77/CAM | 100001932 |
|
||||
| 236253 | 100899549(v6) | 21/r77/CAM | 100001472 |
|
||||
| 236257 | 117676769(v7) | 22/r78/LM | 100001043 |
|
||||
|
||||
### .105 read-only verdict (PROVEN 2026-08-14) — UNRECOVERABLE, confirmed
|
||||
SSH read-only to the FIFA machine (10.10.0.105). Install `/mnt/games/FIFA 17`
|
||||
is retail PC (FIFA17.exe, build 2017-06-09, changelist 3175939, sku FFA17PCC).
|
||||
The resident FIFA17.exe database (recon `data/tables`) shows the DLC/Legend name
|
||||
tables ship ZERO rows in the retail PC build:
|
||||
- `dcplayernames.json`: rowcount:0, rows_emitted:0 (nameid range 30000-35000 = DLC names)
|
||||
- `editedplayernames.json`: rowcount:0, rows_emitted:0
|
||||
- `playernames.json`: 24314 rows (standard players only)
|
||||
FUT Legends were Xbox-One-exclusive in FIFA 17; the PC client contains no Legend
|
||||
names at all. The 13 Legend instances are therefore GENUINELY UNRECOVERABLE from
|
||||
any PC install (.105 == .120, same retail build) — NOT a filtering artifact and
|
||||
NOT fabricated. Architecture is already correct: they auto-promote the instant a
|
||||
name row is supplied (e.g. from an Xbox FIFA17 DB), with no importer code change.
|
||||
## Implementation
|
||||
- Consumables+staff: NEW emit path in openfut-import-fifa17 (classify already buckets;
|
||||
plan_definitions only ingests PlayerCard). Add definition builders + Core CardDefinition
|
||||
rows to fifa17-production-cards.json + owned instances in apply.rs. Data 100% present.
|
||||
- Legends: attempt read-only .105 Legends locale research; if names recoverable add roster
|
||||
rows (auto-promote). Otherwise document as unrecoverable (do NOT fabricate names).
|
||||
@@ -1,121 +0,0 @@
|
||||
# FIFA 17 "FUT Squad Update" download failure — root cause
|
||||
|
||||
**Status:** FIXED in the cert generators (commit `fbc0da2`) — the SAN now carries the
|
||||
advertised IP. Verified without the client (a verifying TLS client rejects the old
|
||||
DNS-only cert by IP and accepts the new one; the entrypoint reconcile is idempotent).
|
||||
Live confirmation needs the production container rebuilt with `OPENFUT_ADVERTISE` set,
|
||||
which is operator-gated. See "Fix options (applied)" below.
|
||||
|
||||
**Symptom (client):** entering the FUT hub shows
|
||||
|
||||
> An error occurred downloading the FUT Squad Update. Please try again.
|
||||
|
||||
Recovered from the live client's memory with `scripts/client-error-string.py`, not from a
|
||||
log. Nothing server-side reports an error: every UTAS route answers 200 and the Blaze
|
||||
session stays healthy, answering PINGs while the client sits on the dialog.
|
||||
|
||||
## What it is
|
||||
|
||||
"Squad Update" here is the **roster update**, not the player's lineup. The Blaze responder
|
||||
advertises it as the FUT loading gate (`blaze_responder_v3b.py`):
|
||||
|
||||
```python
|
||||
ROSTER_HOST = "%s:8081" % _ADVERTISE
|
||||
("ROSTERUPDATE_URL", "https://%s/fifa17/fut/rosterupdate.xml" % ROSTER_HOST)
|
||||
("ROSTER_URL", "https://%s/fifa17/roster/" % ROSTER_HOST)
|
||||
```
|
||||
|
||||
The client fetches that URL over TLS, **by IP**, from `roster_server.py`.
|
||||
|
||||
## Root cause
|
||||
|
||||
The client completes TCP, sends a ClientHello, receives the certificate, and then
|
||||
**aborts the handshake itself**. Captured and decoded from the wire:
|
||||
|
||||
```
|
||||
CLIENT: ClientHello client_version = TLS1.2
|
||||
offers 8 suites: RSA_AES_256_GCM_SHA384, RSA_AES_128_GCM_SHA256,
|
||||
RSA_AES_256_CBC_SHA256, RSA_AES_128_CBC_SHA256, RSA_AES_256_CBC_SHA,
|
||||
RSA_AES_128_CBC_SHA, RSA_RC4_128_SHA, RSA_RC4_128_MD5
|
||||
SERVER: ServerHello TLS1.2 -> Certificate (909 B) -> ServerHelloDone
|
||||
CLIENT: Alert level=fatal desc=certificate_unknown
|
||||
```
|
||||
|
||||
Protocol and cipher selection are **compatible** — TLS1.2 with an RSA suite is offered and
|
||||
chosen. The client rejects the **certificate**:
|
||||
|
||||
```
|
||||
subject/issuer = CN = winter15.gosredirector.ea.com (self-signed, CA:TRUE)
|
||||
SAN = DNS:winter15.gosredirector.ea.com, DNS:*.gosredirector.ea.com, DNS:*.ea.com
|
||||
notBefore = Aug 13 01:12:30 2026
|
||||
sha256 = FF:66:69:48:E2:02:AB:F4:A4:40:78:B1:8C:E1:F9:65:96:32:BB:FF:1F:3D:CC:BC:07:54:02:11:8F:4D:12:3F
|
||||
```
|
||||
|
||||
Two facts matter:
|
||||
|
||||
1. **The SAN carries DNS names only, no `iPAddress` entry**, while the advertised URL is an
|
||||
IP literal (`https://10.10.0.120:8081/...`). A DNS-only SAN cannot match an IP dial.
|
||||
2. **The certificate was regenerated on Aug 13.** `roster_server.py` loads the same
|
||||
`redir_cert.pem`/`redir_key.pem` as the Blaze redirector, so the redirector rejects
|
||||
identically — visible as `REDIR REJECTED ... TLS/SSL connection has been closed (EOF)`
|
||||
in the Blaze log. That is tolerated only because the client then reaches Blaze main over
|
||||
plain TCP; the roster fetch has no such fallback, so it is where the failure surfaces.
|
||||
|
||||
`roster_server.py` still carries the comment *"ProtoSSL cert-verify is patched (autopatch),
|
||||
so our self-signed cert is accepted."* That assumption no longer holds for this path.
|
||||
autopatch demonstrably patched both gates in the failing process — the log shows
|
||||
`pid 56298: PATCHED cert gates` and the live bytes read back as the patched patterns
|
||||
(`909090909090` at `0x146132548`, `31c0c3` at `0x1461361b0`) — and the client still sent
|
||||
`certificate_unknown`. So **those two gates do not govern this validation**; either another
|
||||
check exists on the roster path, or it validates before reaching them.
|
||||
|
||||
## Ruled out, with evidence
|
||||
|
||||
| Hypothesis | Evidence against |
|
||||
| --- | --- |
|
||||
| Missing/mistyped UTAS field | 0 structural differences across 14 hub routes vs production (`scripts/hub-diff-prod-staging.py`) |
|
||||
| Player squad shape | matches production field-for-field, including `squadType`, `custom`, `chemistry`, `manager` |
|
||||
| Squad save | PUT/GET round-trips exactly, every field and slot |
|
||||
| Cert gates unpatched | autopatch log + live byte read both confirm patched |
|
||||
| Roster server broken | serves `200`, `application/xml` over TLS1.2 `AES256-GCM-SHA384` |
|
||||
| Advertised host wrong | production and staging Blaze envs are identical for `OPENFUT_ADVERTISE`, `POW_*`; the scripts are byte-identical |
|
||||
| TLS version mismatch | client offers TLS1.2 and the server selects it |
|
||||
| Blaze session | healthy, PING/PING_REPLY throughout |
|
||||
|
||||
## Probing gotcha
|
||||
|
||||
A default modern Python TLS context gets `SSLV3_ALERT_HANDSHAKE_FAILURE` against this
|
||||
server and looks like a server fault. It is not — the server requires legacy-compatible
|
||||
cipher selection. Probe with `ALL:@SECLEVEL=0`, and note it rejects TLS1.0/1.1 outright
|
||||
(`TLSV1_ALERT_PROTOCOL_VERSION`), accepting only TLS1.2.
|
||||
|
||||
## Why production appears unaffected
|
||||
|
||||
Unresolved, and worth stating plainly rather than guessing. Production advertises the same
|
||||
URL, backed by the same server and certificate, so the same rejection should occur. The
|
||||
operator reports production is clean. The likely explanation is that a production client
|
||||
does not re-fetch the roster (already satisfied), while a session against a different Blaze
|
||||
forces the fetch — but that has not been measured. The known-good hook log used as a
|
||||
control (Aug 12 19:13) **predates the Aug 13 certificate regeneration**, so it is not
|
||||
evidence about the current certificate.
|
||||
|
||||
## Fix options — option 1 applied (`fbc0da2`)
|
||||
|
||||
1. **Reissue the certificate with an `iPAddress` SAN** — **APPLIED.** The three cert
|
||||
generators now put the advertised IP in the SAN: the docker entrypoint reconciles it at
|
||||
startup from `OPENFUT_ADVERTISE` (runtime value, unknown at build time; reissued only
|
||||
when missing, so restarts don't churn the fingerprint), the Dockerfile keeps `openssl`
|
||||
in the runtime image and bakes a loopback-IP baseline, and `openfut-fut.sh` defaults the
|
||||
SAN IP to the host's primary LAN IP. Smallest change, benefits the redirector too. Note
|
||||
`openfut-tls` only *loads* the cert — it does not generate it — so the generation fix
|
||||
lives in these three scripts regardless of whether the roster is served by Python or the
|
||||
Rust `openfut-roster-host`.
|
||||
2. **Serve the roster over plain HTTP** — not taken. Sidesteps TLS entirely but only
|
||||
acceptable off production, and option 1 fixes it properly everywhere.
|
||||
3. **Find the real validation site** on the roster path and extend the patch set — not
|
||||
needed once the SAN matches; kept on record as the most-faithful alternative.
|
||||
|
||||
Option 1 was chosen because it is testable without the client: a verifying TLS client
|
||||
checking the cert by IP fails on the old cert and passes on the new one. That test now
|
||||
passes (`scripts`-style probe run at fix time); the remaining step is the operator
|
||||
rebuilding the production container so the reissued cert is actually served.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,241 +0,0 @@
|
||||
# FIFA 17 seller-facing SOLD state — static recovery from CardsDLL
|
||||
|
||||
Reverse engineering of `CardsDLL_Win64_retail.dll`
|
||||
(md5 `4de3493131d7d2ff7f8b360c5ac9b655`), Ghidra 12.1.2 headless via pyghidra,
|
||||
13,382 functions, image base `0x180000000`. Queries and raw output:
|
||||
`docs/evidence/market-sold-re-2026-08-17/`.
|
||||
|
||||
Confidence tags: **[PE]** read from the binary · **[PE-PROOF]** a proven *negative*
|
||||
(an exhaustive absence) · **[PLAN]** corpus prose, not a decompile · **[UNDECIDABLE]**
|
||||
proven not answerable from this binary.
|
||||
|
||||
---
|
||||
|
||||
## 1. There is no `sold` token, and now it is exhaustive
|
||||
|
||||
Both vocabularies dumped in full to their sentinels, not sampled. **[PE]**
|
||||
|
||||
`tradeState` table `0x180229e40` — exactly 4 rows, then a `{0,0}` terminator:
|
||||
|
||||
```text
|
||||
active=1 inactive=2 expired=3 closed=4
|
||||
```
|
||||
|
||||
`itemState` table `0x180229cc0` — exactly 12 rows, then a `{0,-1}` terminator:
|
||||
|
||||
```text
|
||||
invalid=0 free=1 WAITING_FOR_GAME=2 inGame=2 forSale=5 offered=6
|
||||
activeBadge=100 activeHomeKit=101 activeAwayKit=102 activeBall=103
|
||||
activeStadium=104 active=255
|
||||
```
|
||||
|
||||
Neither contains `sold`. So the seller's sold state **must** be a combination of
|
||||
existing atoms. This closes the question that previously rested on a partial dump.
|
||||
|
||||
## 2. What `closed` actually does — the complete flag computation
|
||||
|
||||
Decompiled from the `auctionInfo` deserializer `0x18013e410`. `local_70` is
|
||||
`tradeState`, `local_40` is `bidState`. **[PE]**
|
||||
|
||||
```c
|
||||
if (local_70 == 4) { /* tradeState == closed */
|
||||
local_3b = local_40 != 0; /* bidState != none */
|
||||
} else {
|
||||
local_3b = (local_40 - 1U & 0xfffffffd) == 0; /* bidState in {1,3} */
|
||||
}
|
||||
local_3a = local_40 - 2U < 2; /* bidState in {2,3} */
|
||||
```
|
||||
|
||||
Evaluated:
|
||||
|
||||
| | `none`(0) | `outbid`(1) | `highest`(2) | `buyNow`(3) |
|
||||
|---|---|---|---|---|
|
||||
| `local_3b`, tradeState==closed | 0 | **1** | **1** | **1** |
|
||||
| `local_3b`, otherwise | 0 | 1 | 0 | 1 |
|
||||
| `local_3a` (any tradeState) | 0 | 0 | **1** | **1** |
|
||||
|
||||
## 3. The complete record → Flash mapping
|
||||
|
||||
From the publisher `0x1801bf030`, every property it sets, with its record offset.
|
||||
This supersedes the previous partial list. **[PE]**
|
||||
|
||||
| Flash property | record | meaning |
|
||||
|---|---|---|
|
||||
| `TRADEID_LOWER` / `TRADEID_UPPER` | +0x38 | tradeId, split into two 32-bit halves |
|
||||
| `DURATION` | +0x90 | formatted; **`FUT_AUCTION_EXPIRED`** when the value underflows (i.e. `expires == 0`) |
|
||||
| `TIME_REMAINING` | +0x90 | expires, seconds |
|
||||
| `MIN_CREDITS` | +0x78 | currentBid |
|
||||
| `MAX_CREDITS` | +0x70 | buyNowPrice |
|
||||
| `RESERVEDPRICE` | +0x74 | startingBid |
|
||||
| `YOURBID` | +0xb8 | **bidState, passed through verbatim** |
|
||||
| `STATE` | +0x88 | **tradeState, passed through verbatim** |
|
||||
| **`COINS_AWARDED`** | **+0xbf** | **the `coinsProcessed` atom (0x2f4), u8** |
|
||||
| `UUID_UPPER` / `UUID_LOWER` | itemData+8 | |
|
||||
| `CARD_ID` / `FIFA_ID` | itemData+0x18 | `FIFA_ID` masks `& 0xffffff` |
|
||||
| `CARD_TYPE` | itemData+0x4c | |
|
||||
| `CARD_OFFERSTATE` | itemData+0x5c | itemState |
|
||||
| `IS_WATCHED` | +0xbc | `watched` atom |
|
||||
| `INBOX` | +0xbe | `local_3a` — bidState ∈ {highest, buyNow} |
|
||||
| `IS_GLOW` | +0xbd | `local_3b` — the table in §2 |
|
||||
| `TRADE_DATA_AVAILABLE` | — | constant 1 |
|
||||
|
||||
**`coinsProcessed`'s consumer is now traced.** The corpus recorded its type and
|
||||
noted that no consumer had ever been found; it is published to the movie as
|
||||
**`COINS_AWARDED`**. That is a settlement/"you have been paid" signal, exactly as
|
||||
the corpus guessed but never demonstrated.
|
||||
|
||||
## 4. `highest` vs `buyNow` on a closed row is UNDECIDABLE from CardsDLL
|
||||
|
||||
**[UNDECIDABLE]**, and this is a proof, not a failed search.
|
||||
|
||||
For `tradeState == closed`, §2 gives `IS_GLOW = (bidState != none)` and
|
||||
`INBOX = (bidState ∈ {highest, buyNow})`. Both `highest`(2) and `buyNow`(3)
|
||||
therefore produce **`IS_GLOW=1, INBOX=1`** — bit-identical. No native consumer can
|
||||
tell them apart.
|
||||
|
||||
But §3 sharpens *why* it is undecidable: `bidState` is **not** consumed only through
|
||||
those flags. It is published verbatim as `YOURBID`, alongside `STATE` and
|
||||
`COINS_AWARDED`. The movie receives the raw values. So the discrimination exists —
|
||||
it just lives entirely in the APT/ActionScript front end, which is unread.
|
||||
|
||||
Consequence: **no amount of further CardsDLL work can answer "which `bidState` does
|
||||
a seller see".** Only an AVM1 read of `external/ion_fut/screens/trading/tradepile`,
|
||||
or a live behavioural A/B, can. This retires the question as a static target.
|
||||
|
||||
The corpus's own lifecycle table (`fifa17-recon/docs/plan-2026-08-06-transfer-market.md:720-729`)
|
||||
asserts the seller sees `closed` / **`highest`** with `coinsProcessed 1`, and assigns
|
||||
`closed` / `buyNow` to the *buyer*. That is **[PLAN]**, and it **contradicts** the
|
||||
common third-party lore that a sold seller row is `closed` + `buyNow`. Given §4 the
|
||||
contradiction cannot be resolved statically — but note the corpus reading is the one
|
||||
that leaves `buyNow` meaning "*I* bought it now", which is self-consistent with
|
||||
`YOURBID` being a property about the viewer's own bid.
|
||||
|
||||
## 5. The clear-sold verb EXISTS — PE-proven
|
||||
|
||||
The request builder `0x1801647c0`: **[PE]**
|
||||
|
||||
```c
|
||||
if (*(longlong *)(param_1 + 0x10) == 0) {
|
||||
FUN_180007f80(&local_38, 0x20, "/sold"); /* no tradeId -> bulk */
|
||||
} else {
|
||||
FUN_180007f80(&local_38, 0x20, "/%lld"); /* one specific tradeId */
|
||||
}
|
||||
```
|
||||
|
||||
One builder, two forms, on route base `ut/delete/%s/trade` (`DELETETRADE`), response
|
||||
class `RS4:FutISRemoveTradeServerResponse` (`0x180228bc8`, with the `/sold` literal
|
||||
at `0x180228bec` immediately after it):
|
||||
|
||||
```text
|
||||
DELETE ut/delete/{ns}/trade/{tradeId} remove one trade
|
||||
DELETE ut/delete/{ns}/trade/sold remove ALL sold trades
|
||||
```
|
||||
|
||||
Corroborated by the client's own request-name table, where
|
||||
**`RemoveAllSoldFromTradePile`** (`0x1801efae8`) sits beside `RemoveFromTradePile`,
|
||||
`AddToWatchList` and `RemoveFromWatchList`.
|
||||
|
||||
**Architectural consequence.** A bulk "remove all sold" verb only makes sense if
|
||||
sold rows **persist in the seller's pile until explicitly cleared**. That is
|
||||
incompatible with our current host, where a sold/cancelled listing leaves
|
||||
`/tradePile` the instant the CAS lands (the Fix A invariant). Implementing the sold
|
||||
path will require revisiting that invariant — and doing so needs live validation,
|
||||
because Fix A itself was a live-confirmed correction.
|
||||
|
||||
## 6. The seller-facing SOLD counter is real, and we hardcode it to 0
|
||||
|
||||
A complete chain, wire atom → struct → Flash → localised caption, with no inference
|
||||
at any step. **[PE]**
|
||||
|
||||
Hub `tradePile` sub-deserializer `0x18013ead0`:
|
||||
|
||||
| atom | id | writes | Flash slot (publisher `0x1800b1dc0`, tile `0x1c0`) |
|
||||
|---|---|---|---|
|
||||
| `count` | 0xbc | +0x1d4 | `TEXT0` with caption `FUT_UC_ITEMS` |
|
||||
| `notification` | 0x1da | +0x1d6 | `NOTIFICATION`, capped at 99 |
|
||||
| `selling` | 0x2b8 | +0x1d2 (**and** counts-struct +0x36) | `TEXT2` with caption `FUT_TF_SELLING` |
|
||||
| **`sold`** | **0x2c9** | **+0x1d8** | **`TEXT3` with caption `FUT_TF_SOLD`** |
|
||||
|
||||
The sibling tile `0x1d0` (Transfer Targets) uses `FUT_TF_WINNING` (+0x1c8) and
|
||||
`FUT_TF_OUTBID`. `selling` writing two structs independently re-confirms the
|
||||
corpus's counts-struct offset for `selling`.
|
||||
|
||||
So FIFA 17 renders a **SOLD** count to the seller, sourced from atom `sold` (0x2c9).
|
||||
Our host and the Python oracle both hardcode `sold: 0`, so that bucket can never
|
||||
populate. This is direct client evidence about `sold` — the thing
|
||||
`MARKET_SOLD_SETTLEMENT.md` required before touching `/tradePile/counts`. It
|
||||
establishes that `sold` is *displayed*; it does **not** yet establish what should be
|
||||
counted in it (rows awaiting clear? rows sold this session?).
|
||||
|
||||
## 7. Atom-name decoder (method note, reusable)
|
||||
|
||||
Atom IDs are **indices into an alphabetically sorted pointer table** of atom-name
|
||||
strings, base `0x1802d2760`. Validated against all twelve known `auctionInfo`
|
||||
atoms — 12/12 agree — and cross-checked against `fifa17-recon/docs/fut_atoms.tsv`.
|
||||
**[PE]**
|
||||
|
||||
```text
|
||||
atom_id = (pointer_slot_address - 0x1802d2760) / 8
|
||||
```
|
||||
|
||||
Newly resolved: `sold`=0x2c9, `count`=0xbc, `offered`=0x1e5, `selling`=0x2b8,
|
||||
`maxAuctionsAllowed`=0x1bf, `credits`=0xc0, `auctionInfo`=0x35, `total`=0x325,
|
||||
`duplicateItemIdList`=0xec, `itemState`=0x172, `offers`=0x1e6, `coins`=0x95.
|
||||
|
||||
Caution when using it: look up a name by finding the pointer slot **inside the table
|
||||
range**, not by taking the first matching string in the binary. Common words such as
|
||||
`sold`, `offered` and `count` appear in several unrelated tables, and taking the
|
||||
first hit produces confident nonsense (it initially reported `sold` as ABSENT and
|
||||
`offered` as a negative index).
|
||||
|
||||
## 8. An auction-outcome vocabulary exists but this client ignores it
|
||||
|
||||
The atom table contains a 7-value outcome vocabulary: **[PE]**
|
||||
|
||||
```text
|
||||
0x36 auctionLostBidRejected 0x39 auctionSoldBid 0x3b auctionWonBid
|
||||
0x37 auctionLostOutbid 0x3a auctionSoldBuyNow 0x3c auctionWonBuyNow
|
||||
0x38 auctionLostOutbidSelf
|
||||
```
|
||||
|
||||
These would distinguish seller-sold-by-bid from seller-sold-by-buy-now explicitly.
|
||||
**No CardsDLL deserializer consumes them.** Every candidate function that compares
|
||||
against three or more of `0x36..0x3c` was checked and none is an atom dispatcher
|
||||
(no value-SKIP `0x180135ff0`, no atom loop `0x1801c7f10`) — they are small-immediate
|
||||
coincidences. **[PE-PROOF]** So the vocabulary is server-side or telemetry, and it
|
||||
does not carry the sold state to this client.
|
||||
|
||||
## 9. The 5% fee is NOT in the client — Task B is undecidable from here
|
||||
|
||||
**[PE-PROOF]**, three independent absences:
|
||||
|
||||
* no `0.95` or `0.05` constant, `double` or `float`, anywhere in the binary;
|
||||
* no localisation key for tax/fee/net/proceeds/commission/"you will receive" — the
|
||||
only `FUT_TF_*` keys in the binary are `SELLING`, `SOLD`, `WINNING`, `OUTBID`;
|
||||
* the 17 functions using both `5`/`95` and `100` as immediates are all unrelated
|
||||
(they include the item deserializer) — no fee arithmetic exists.
|
||||
|
||||
The client therefore never computes or displays a net. It learns the seller's
|
||||
balance only from `credits`.
|
||||
|
||||
**This makes the rounding rule unmeasurable by experiment against our own server.**
|
||||
Whatever we credit is what the client displays; there is no client-side expectation
|
||||
to compare against, so there is no oracle. The only evidence that could settle
|
||||
floor-the-fee (150 → 143) versus floor-the-proceeds (150 → 142) is an original
|
||||
EA-era capture of a seller's balance across a known-price sale, which we do not
|
||||
have.
|
||||
|
||||
Accordingly the rule stays a **documented choice**: fee = `floor(gross × 5 / 100)`,
|
||||
proceeds = `gross − fee`, chosen because `fee + proceeds == gross` holds exactly at
|
||||
every input. It is pinned by regression tests at 0, 1, 19, 20, 21, 39, 40, 100, 101,
|
||||
119, 120, 149, 150, 151, 199, 200, 1 000, 15 000, 15 000 000 and `i64::MAX`, so it
|
||||
cannot drift silently.
|
||||
|
||||
## 10. What would close the remaining gaps
|
||||
|
||||
| Gap | Only remaining route |
|
||||
|---|---|
|
||||
| `highest` vs `buyNow` on a seller's sold row | AVM1 disassembly of `tradepile.isInActiveAuction` / `PreCheckCardOptions`, or a live behavioural A/B (the movie gets `YOURBID` verbatim, so the two ARE separable by the client's behaviour) |
|
||||
| What `sold` should count | live observation with a real sold row |
|
||||
| Whether sold rows persist until cleared | live: does the client issue `DELETE .../trade/sold`? |
|
||||
| The 5% rounding rule | an original EA-era seller-balance capture; nothing in reach |
|
||||
@@ -1,887 +0,0 @@
|
||||
# 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/<sku>/trade/<id>` | **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/<sku>/trade/<id>` | **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.
|
||||
|
||||
---
|
||||
|
||||
## MEASURED in the live client — 2026-08-17
|
||||
|
||||
Read out of the running `FIFA17.exe` (pid-resolved, CardsDLL slide proven against
|
||||
the on-disk FNV prologue) with `fifa17-recon/tools/trade_gate_probe.py`, which
|
||||
extends `gate_byte_probe.py` to vtable slot `+0x270` as the transfer-market
|
||||
analysis asked for. Read-only: `/proc/<pid>/mem` `O_RDONLY` + `pread`.
|
||||
|
||||
| Gate | Python era (2026-08-06) | Now | Owner |
|
||||
|---|---|---|---|
|
||||
| `IS_TRADING_ENABLED` `model+0x1fd2e` (slot `+0x270`) | **0** | **1** | settings struct `+0x28`; was zeroed by `userInfo.feature.trade` |
|
||||
| `TRADE_PILE_SIZE` `model+0x1fd1c` | **0** | **100** | `userMassInfo.pileSizeClientData` key 2 |
|
||||
| watch-list size `model+0x1fd20` | **0** | **50** | same member, key 4 |
|
||||
| `storeEnabled` `model+0x1fd2f` | 1 | 1 | control |
|
||||
| `IS_FRIENDLY_SEASON` / `IS_DRAFT_MODE` / `packOpeningAnimation` | 1 | 1 | controls |
|
||||
|
||||
**CONFIRMED: every CardsDLL-supplied input the transfer-market analysis named as a
|
||||
blocker is now open.** The Rust host does this by construction — it emits
|
||||
`userInfo.feature` as `{}` (no `trade` member, so the kill switch at `0x180174f19`
|
||||
never arms: it fires only when atom `0x330` inside `0x11c` parses as exactly 1) and
|
||||
it already sends `pileSizeClientData` keys 2 and 4. Serving `tradingEnabled: 1` in
|
||||
the settings `configs` array would NOT have worked, because that tail runs after
|
||||
every member is parsed and would overwrite it.
|
||||
|
||||
### What this rules out
|
||||
|
||||
The Transfer List Actions panel not opening on an own listing is therefore **not**:
|
||||
|
||||
* an ownership field — FIFA 17's auctionInfo has no `tradeOwner`/`sellerId` atom;
|
||||
* `IS_TRADING_ENABLED`, `TRADE_PILE_SIZE` or the watch-list size — all measured open;
|
||||
* the cancel route — `DELETE ut/delete/{ns}/trade/{tradeId}` is the PE's spelling and
|
||||
is what we serve;
|
||||
* `tradeState` / `bidState` / `expires` spellings — all three are the PE's own
|
||||
vocabularies and values.
|
||||
|
||||
Per the analysis's own falsifier ("if the byte reads 1 and the screen still refuses,
|
||||
the exe-side predicate has a term we have not enumerated"), the remaining term is
|
||||
**exe-side UI script**, which CardsDLL does not own and the server cannot set.
|
||||
Status: **UNKNOWN**, and it is now the narrowest it has ever been.
|
||||
|
||||
### Confirmed fidelity bug found on the way
|
||||
|
||||
`expires` was a frozen `3600` on every poll, so the client's live countdown never
|
||||
moved and an auction could never run out. Now derived from `created_at + duration`
|
||||
(duration taken from the `ISStart` body), clamped at 0, with an aged-out active
|
||||
listing projecting as `expired`/`none` — FIFA 17's relistable state. Verified live:
|
||||
the standing listing correctly reads `expires: 0` once past its hour.
|
||||
|
||||
---
|
||||
|
||||
## RESOLVED against the live client — 2026-08-17 (supersedes the tags above)
|
||||
|
||||
Driven by the real FIFA 17 client end to end: list → expire → **relist** → active,
|
||||
with the operator confirming each UI state. Live-client behaviour OUTRANKS both the
|
||||
Python oracle and contemporaneous Web App implementations wherever they disagree.
|
||||
|
||||
### `tradeOwner` — do not re-litigate this
|
||||
|
||||
```text
|
||||
tradeOwner exists in the FIFA 17-era FUT API:
|
||||
FIFA17-HISTORICAL
|
||||
|
||||
tradeOwner required by FIFA17.exe Transfer List Actions:
|
||||
DISPROVEN for the current client path
|
||||
```
|
||||
|
||||
It is absent from the twelve atoms FIFA 17's `auctionInfo` deserializer
|
||||
(`0x18013e410`) reads, so the client value-SKIPs it at `0x180135ff0`. It was
|
||||
implemented, deployed, observed to change nothing, and REMOVED. A future agent
|
||||
rediscovering the old Web App sources will find this note before spending
|
||||
deployments on it again.
|
||||
|
||||
### What actually gated the Actions panel — all lifecycle/state bugs
|
||||
|
||||
```text
|
||||
CONFIRMED — FIFA17.exe live client
|
||||
|
||||
Own Transfer List interactivity does NOT depend on tradeOwner in this path.
|
||||
|
||||
trade/status polling is LOAD-BEARING for Transfer List state. The tail carries no
|
||||
numeric id, so it fell through the `trade…` buy/view arm and answered every poll
|
||||
with an empty auctionInfo. Route is ISVIEWTRADE:
|
||||
GET ut/{ns}/trade/status?tradeIds=a,b,c
|
||||
|
||||
auctionInfo.expires is SECONDS REMAINING *and must evolve with wall-clock time*.
|
||||
A structurally valid but FROZEN value breaks lifecycle behaviour: the auction never
|
||||
ages into expired/relistable, which is the state where Re-list appears.
|
||||
|
||||
Relisting must PERSIST. FIFA 17 relists by re-sending ISStart (POST /auctionhouse)
|
||||
for an item that already has a listing row, so the primary-key conflict IS the
|
||||
relist. Swallowing it as success left the stale expired row intact and produced a
|
||||
client-visible lifecycle failure behind an HTTP 200.
|
||||
```
|
||||
|
||||
### Known-good own active auction (frozen fixture)
|
||||
|
||||
Captured at `docs/evidence/market-lifecycle-2026-08-17/`, including
|
||||
`_index.json._countdown_proof` which records `expires` decrementing (`frozen:
|
||||
false`) so the live clock is machine-checkable rather than asserted in prose.
|
||||
|
||||
```text
|
||||
tradeState = active
|
||||
bidState = none
|
||||
expires = decrementing (seconds remaining)
|
||||
sellerName = CAGE
|
||||
auction record = 12 atoms
|
||||
response envelope = 4 members (auctionInfo, credits, duplicateItemIdList, total)
|
||||
ISViewTrade body = 2 members (auctionInfo, credits)
|
||||
pricelimits = BARE ARRAY (container type is load-bearing)
|
||||
```
|
||||
|
||||
**Do not "improve" this shape without a live-client retest.**
|
||||
|
||||
### The development rule this established
|
||||
|
||||
A response can be structurally plausible, pass differential parity, and render
|
||||
perfectly while still being behaviourally wrong, because FIFA expects an **evolving
|
||||
server-side state machine** rather than a static object that looks like one. The
|
||||
frozen `expires` is the canonical example: every field was the right name, type and
|
||||
vocabulary, and the feature was still broken.
|
||||
|
||||
---
|
||||
|
||||
## INVESTIGATION CLOSED — active own auction is not seller-actionable
|
||||
|
||||
**Do not reopen without new direct FIFA17.exe evidence contradicting the lifecycle
|
||||
below.** The correct FIFA 17 lifecycle is:
|
||||
|
||||
```text
|
||||
ACTIVE AUCTION tradeState=active, expires>0/counting down
|
||||
-> seller CANNOT withdraw it through Transfer List actions
|
||||
-> the item is not seller-actionable while the auction runs
|
||||
|
||||
EXPIRED UNSOLD tradeState=expired, expires=0
|
||||
-> the item becomes actionable again
|
||||
-> relist / return-to-club / other expired-item actions
|
||||
```
|
||||
|
||||
### Confidence tags
|
||||
|
||||
```text
|
||||
CONFIRMED — live FIFA17.exe:
|
||||
active listing with ticking expires is NON-selectable
|
||||
expired listing IS selectable
|
||||
relisting makes it active and therefore non-selectable again
|
||||
no cancel request is ever emitted by the client
|
||||
expires must advance with the wall clock
|
||||
no button prompt is offered on the Transfer List for an active auction
|
||||
|
||||
CONFIRMED — local RE corpus:
|
||||
every CardsDLL-side prerequisite passes (IS_TRADING_ENABLED=1,
|
||||
TRADE_PILE_SIZE=100, watch-list=50, item+0x49 tradeable)
|
||||
MAY_BE_REMOVED is a CONSTANT 1 — it cannot be the gate and the server
|
||||
cannot move it
|
||||
the eight-flag array FUN_18003e370 publishes is the CLUB-CARD Actions menu
|
||||
and contains NO transfer-auction cancellation flag
|
||||
the auction parser is limited to the known twelve atoms
|
||||
|
||||
HISTORICAL FUT / FIFA17-era:
|
||||
active auctions are committed until sale or expiry
|
||||
expired Transfer List items expose relist/return actions
|
||||
FIFA 17 trading guidance tells players to relist once auctions expire
|
||||
|
||||
UNKNOWN, and no longer required for backend fidelity:
|
||||
the exact Flash/ActionScript branch that makes active cards non-selectable
|
||||
```
|
||||
|
||||
### Explicitly out of scope now
|
||||
|
||||
Do NOT add auction fields, change `itemState`, revisit `tradeOwner`, probe
|
||||
`MAY_BE_REMOVED`, add an active-auction cancel feature, or disassemble Flash in
|
||||
order to make active auctions selectable. Three of those were already tried and
|
||||
refuted; the rest are ruled out above.
|
||||
|
||||
### Return-to-club transition (implemented)
|
||||
|
||||
A pile move to `club` now ENDS any `active` auction on that item. Without it the
|
||||
pile reads `club` while the listing row stays `active`, so the card is filtered out
|
||||
of `/club` (exclusion keys on active listings) AND still rendered in the Transfer
|
||||
List — the move silently appears to do nothing. `reserved` (mid-sale) and `sold`
|
||||
rows are never touched, so a card can never be both sold and returned.
|
||||
|
||||
---
|
||||
|
||||
## Q2 — the unlisted transfer-list item, MEASURED 2026-08-17
|
||||
|
||||
The operator moved a card Club -> Transfer List **without listing it**
|
||||
(`PUT /ut/game/fifa17/item`, no `POST /auctionhouse`). State captured immediately,
|
||||
read-only. Fixture: `docs/evidence/market-lifecycle-2026-08-17/`.
|
||||
|
||||
Server truth at capture: trade pile held **6** items, of which **1** had an active
|
||||
auction and **5** were unlisted.
|
||||
|
||||
| Surface | Unlisted item | Actively-listed item (contrast) |
|
||||
|---|---|---|
|
||||
| `/tradePile` `auctionInfo` | **absent** | present, `tradeState: active` |
|
||||
| `/tradePile/counts` | **not counted** (`count 1, selling 1`) | counted |
|
||||
| `/club` | **present**, `clubPlayers` unchanged at 1965 | **absent** (excluded) |
|
||||
| `itemData.itemState` | `free` | `listFS` |
|
||||
| `itemData.pile` | **field not emitted at all** | not emitted |
|
||||
| `itemData.untradeable` | `false` | `false` |
|
||||
|
||||
**Finding: we do not represent the unlisted state anywhere on the wire.** Such an
|
||||
item is byte-identical to a club item; only the host's own pile store knows it is in
|
||||
the trade pile, and nothing the client receives says so.
|
||||
|
||||
Still **UNKNOWN** (do not guess): the FIFA 17 encoding of that state. The PE
|
||||
documents returned `itemData.pile` as NUMERIC with an unrecovered mapping, and
|
||||
`tradeState` decodes through a closed table walk (`active=1 inactive=2 expired=3
|
||||
closed=4`) where an unrecognised `bidState` is silently swallowed as `none` — so a
|
||||
wrong enum yields a plausible-looking but WRONG UI. The corpus explicitly warns that
|
||||
`inactive` decodes but "no client path treats it specially; do not emit it".
|
||||
|
||||
Open discriminator, needs the client only: after backing out of FUT and re-entering,
|
||||
does the Transfer List still show an unlisted item? If it does not, the move is not
|
||||
durable from the client's point of view and the state must be represented; if it
|
||||
does, `tradePile` is auctions-only by design and there is nothing to fix.
|
||||
|
||||
---
|
||||
|
||||
## Domain boundary: pile membership vs auction lifecycle
|
||||
|
||||
Two SEPARATE facts, but several mutations require a COORDINATED transition. Both
|
||||
limbo bugs this session came from conflating them, so this is modelled explicitly
|
||||
and covered by tests rather than patched route by route.
|
||||
|
||||
| Mutation | Pile | Auction |
|
||||
|---|---|---|
|
||||
| Club -> Transfer List | becomes `trade` | none necessarily exists |
|
||||
| list item | stays `trade` | becomes `active` |
|
||||
| clock runs out unsold | stays `trade` | becomes `expired` (projection; no row mutated) |
|
||||
| relist | stays `trade` | `active` again, clock restarted, new prices |
|
||||
| return expired item to Club | becomes `club` | any `active` association MUST end |
|
||||
| sold / reserved | — | MUST NOT be ended by a generic move-to-club |
|
||||
|
||||
Invariant, stated so it can be tested rather than remembered: **an item must never be
|
||||
simultaneously excluded from `/club` and absent from `/tradePile`.** That is the limbo
|
||||
state, and it is reachable in two ways, both now closed:
|
||||
|
||||
1. Excluding by *pile* rather than by *active listing* (fixed in `f2c4927`) — the
|
||||
pile can hold items with no auction, and `/tradePile` renders auctions only.
|
||||
2. Moving to `club` while leaving the auction `active` (fixed in `4e31fb9`) — the
|
||||
pile says club, the exclusion still fires, and the card shows in neither place.
|
||||
|
||||
Tests: `returning_an_expired_listing_to_the_club_ends_its_auction`,
|
||||
`a_pile_move_never_disturbs_a_sale_in_flight`,
|
||||
`club_excludes_listed_items_and_paginates_the_visible_set`.
|
||||
|
||||
---
|
||||
|
||||
## Q2 representation — narrowed to ONE candidate by elimination (2026-08-17) — **SUPERSEDED, see the final section**
|
||||
|
||||
Re-entry discriminator result: **CONFIRMED BUG.** After fully leaving and re-entering
|
||||
FUT, the active auction reconstructs correctly under LISTED ITEMS, but the unlisted
|
||||
TRANSFER LIST section is **empty** — item `100000059` does not survive. The
|
||||
immediate post-move visibility was client-local only, so our representation cannot
|
||||
durably reconstruct trade-pile membership.
|
||||
|
||||
The representation is now pinned by elimination over PROVEN facts, not chosen:
|
||||
|
||||
1. **Only one route can own a trade-pile list.** The complete CardsDLL route table
|
||||
(`.rdata 0x18021df80`, 45 routes + 3 empty admin slots) is dumped verbatim at
|
||||
`docs/evidence/market-lifecycle-2026-08-17/cardsdll-route-table.txt` via
|
||||
`fifa17-recon/tools/route_table_dump.py` (static, read-only, VA→offset resolved
|
||||
through the real PE section table). Row 30 `ut/%s/tradePile` is the ONLY
|
||||
trade-pile route. There is no trade-pile *items* route.
|
||||
2. **That route carries only auction records.** `FutGetTradePile` (`0x180170810`)
|
||||
deserializes the shared IS-list body (`0x18013e7f0`), whose `auctionInfo`
|
||||
elements go through `0x18013e410` — the twelve-atom auction record.
|
||||
3. **`pile` cannot be set from the wire.** `pile` (atom 0x226) has NO arm in the item
|
||||
deserializer `FUN_18013fe00` (checked in all four dispatch forms); `item+0x60` is
|
||||
assigned by the OWNING LIST, reading 1 for every `/club` item and 6 for every
|
||||
`/purchased` item. So membership is conferred by *arriving in the list*, never by
|
||||
a field we can add.
|
||||
4. **Of the twelve atoms, only `tradeState` can express lifecycle.**
|
||||
5. **`tradeState`'s vocabulary is closed and has exactly one unused value.** The table
|
||||
walk at `0x180229e40` decodes `active=1 inactive=2 expired=3 closed=4`, anything
|
||||
else `-1`. `active`, `expired` and `closed` are all already spoken for by the
|
||||
observed lifecycle.
|
||||
|
||||
**Therefore an unlisted trade-pile item can only be an `auctionInfo` record with
|
||||
`tradeState: "inactive"`.** That is the sole encoding the client's own parser can
|
||||
accept for "in the pile, no auction" — reached by elimination, not invention.
|
||||
|
||||
Status: **INFERRED-BY-ELIMINATION**, not yet CONFIRMED. One binary question remains,
|
||||
and it is about client *rendering*, not encoding: does the Flash Transfer List place
|
||||
an `inactive` record in the unlisted section? The corpus's warning that "`inactive`
|
||||
decodes but no client path treats it specially" was established for the market
|
||||
PRESENTATION function (`flagA`/`flagB`), where `inactive` is indeed unremarkable —
|
||||
that is consistent with, and does not contradict, using it for list membership.
|
||||
|
||||
Ruled out on evidence, do not retry: adding `itemData.pile` (inert, no deserializer
|
||||
arm), a second route (none exists), and `tradeId: 0` / invented `itemState` values
|
||||
(unnecessary — `tradeState` alone carries the distinction).
|
||||
|
||||
### Acceptance test for whatever lands
|
||||
|
||||
```text
|
||||
move item Club -> Transfer List
|
||||
leave FUT entirely
|
||||
re-enter FUT
|
||||
item appears in the unlisted TRANSFER LIST section
|
||||
item can be returned to Club
|
||||
no auction exists unless explicitly listed
|
||||
```
|
||||
|
||||
Plus the active lifecycle must remain unchanged: active listing under LISTED ITEMS,
|
||||
`expires` counting down, active non-selectable, expiry making it actionable, relist
|
||||
working.
|
||||
|
||||
### Revised domain invariant
|
||||
|
||||
Core pile membership is NOT equivalent to `/tradePile` visibility. Core ownership/pile
|
||||
state and auction lifecycle are separate authoritative facts; the wire may expose them
|
||||
through different FIFA 17 resources. **A transition is complete only when a fresh FIFA
|
||||
session can reconstruct the same user-visible state** — re-entry is the acceptance
|
||||
test, not the immediate post-mutation response.
|
||||
|
||||
---
|
||||
|
||||
## Q2 — LIVE-CONFIRMED: `tradeState: "inactive"` is the unlisted representation — **SUPERSEDED, see the final section**
|
||||
|
||||
Operator-observed in the real FIFA 17 client, 2026-08-17, with the bounded one-item
|
||||
probe (`OPENFUT_FIFA17_UNLISTED_PROBE`). Fixture:
|
||||
`docs/evidence/market-lifecycle-2026-08-17/inactive-row-live-confirmed.json`.
|
||||
|
||||
### The observed `inactive` UI contract
|
||||
|
||||
| Wire | Client presentation |
|
||||
|---|---|
|
||||
| `tradeState: "inactive"` | row placed under the right-hand **TRANSFER LIST** section, NOT under LISTED ITEMS |
|
||||
| `startingBid: 0` | Start Price = `0` |
|
||||
| `buyNowPrice: 0` | Buy Now Price = `-` (absent) |
|
||||
| `currentBid: 0` | Current Bid = `-` (absent) |
|
||||
| `expires: 0` | Time Remaining = `-` (no timer) |
|
||||
| `bidState: "none"` | no bid presentation |
|
||||
| `itemData.itemState: "free"` | renders as a normal owned card |
|
||||
|
||||
An `active` row in the same body continued to render separately under LISTED ITEMS,
|
||||
so the two sections are driven by `tradeState` and the experiment did not disturb the
|
||||
known-good active path.
|
||||
|
||||
Token provenance: dumped from the client's own `{const char*, int}` table at
|
||||
`0x180229e40` (`active=1 inactive=2 expired=3 closed=4`) — see
|
||||
`cardsdll-vocab-tables.txt` and `fifa17-recon/tools/vocab_dump.py`. Nothing here was
|
||||
guessed.
|
||||
|
||||
Regression test: `market::tests::unlisted_candidate_is_a_non_active_pile_row` pins the
|
||||
whole tuple, plus that the row is never emitted for an item outside the `trade` pile
|
||||
and never duplicates a real auction
|
||||
(`unlisted_candidate_never_duplicates_a_real_auction`).
|
||||
|
||||
### Expired -> Club coupled transition, verified
|
||||
|
||||
Operator returned the naturally-expired Ronaldo to the club. Server state after:
|
||||
|
||||
```
|
||||
listing 1000000097 state = cancelled (the coupled transition fired)
|
||||
pile Ronaldo = club
|
||||
/tradePile Ronaldo absent
|
||||
/club Ronaldo present, itemState free
|
||||
hub clubPlayers 1964 -> 1965, auctionCount 1
|
||||
counts {count 1, selling 1, sold 0}
|
||||
```
|
||||
|
||||
All of that is durable store state rather than a client-local view, so it survives a
|
||||
session boundary by construction. Pending the operator's final exit/re-enter
|
||||
confirmation before this is tagged CONFIRMED.
|
||||
|
||||
### Counts observations so far (semantics still UNCHANGED and unresolved)
|
||||
|
||||
| State | `count` | `selling` | `sold` |
|
||||
|---|---|---|---|
|
||||
| 1 active | 1 | 1 | 0 |
|
||||
| 2 active | 2 | 2 | 0 |
|
||||
| 1 active + 1 inactive (probe) | 1 | 1 | 0 |
|
||||
| unlisted pile items (no probe) | not counted | not counted | 0 |
|
||||
|
||||
So `count` currently tracks AUCTION entries, not total Transfer List membership. Do
|
||||
not change this until the full state set (empty / unlisted / active / expired / sold /
|
||||
mixed) has been observed — it remains an open question whether FIFA 17 expects
|
||||
`count` to include non-active pile members.
|
||||
|
||||
---
|
||||
|
||||
## Q2 status downgrade + the "Expired" differential EXPLAINED (2026-08-17) — **SUPERSEDED, see the final section**
|
||||
|
||||
Phase C was a PARTIAL pass. Corrected conclusions:
|
||||
|
||||
```text
|
||||
tradeState "inactive" = CONFIRMED section/lifecycle discriminator
|
||||
(rows persist across a fresh session and land under
|
||||
TRANSFER LIST, not LISTED ITEMS)
|
||||
|
||||
complete ACTIONABLE unlisted representation = was UNKNOWN; see below
|
||||
```
|
||||
|
||||
### The visual differential, and its cause
|
||||
|
||||
| | one-item probe | generalized rows |
|
||||
|---|---|---|
|
||||
| Start Price | 0 | 0 |
|
||||
| Buy Now / Current Bid | `-` | `-` |
|
||||
| Time Remaining | `-` | **`Expired`** |
|
||||
|
||||
Cause: **route coverage, not a field.** `/tradePile` advertised the unlisted
|
||||
tradeIds, but `GET …/trade/status` (ISVIEWTRADE) answered them from the market store
|
||||
ONLY — and an unlisted pile member has no listing row, so the poll returned an empty
|
||||
`auctionInfo`. Observed live as `route=market-status requested=1 returned=0` repeating
|
||||
for the row the operator had selected (`tradeIds=1000000122`), while the same id was
|
||||
present in `/tradePile`.
|
||||
|
||||
The client polls `/trade/status` for the row it is displaying, and an empty answer
|
||||
degrades it: Time Remaining renders `Expired` and no actions are offered. The earlier
|
||||
probe showed `-` simply because the client had not yet polled that id — the logs at
|
||||
the time show only `tradeIds=1000000097`. So `expires`, `tradeState`,
|
||||
`itemData.itemState` and `pile` were all innocent, and NO field was changed.
|
||||
|
||||
This is the same defect class as the original `trade/status` bug: a route the client
|
||||
polls being answered with an empty set. The corpus predicted it — `tradeId` must
|
||||
resolve across `/transfermarket`, `/tradePile`, `/watchList` AND `/trade/status`. We
|
||||
had stability but not coverage.
|
||||
|
||||
Fix: both routes now share ONE pile enumeration (`Server::resolve_trade_pile`), so an
|
||||
id advertised by `/tradePile` always resolves on `/trade/status`. Verified live: the
|
||||
six inactive ids went from `returned=0` to `returned=6`. `/trade/status` still answers
|
||||
only the ids actually asked about, and a real auction always wins over an inactive row
|
||||
for the same id.
|
||||
|
||||
Regression test: `trade_status_resolves_the_unlisted_ids_tradepile_advertises`.
|
||||
|
||||
### Investigations NOT needed as a result
|
||||
|
||||
`itemData.itemState` and numeric `itemData.pile` were queued for PE recovery on the
|
||||
assumption the encoding was incomplete. The cause was route coverage, so neither was
|
||||
touched and neither is implicated. If the actions still do not appear, those remain
|
||||
the next candidates — and `itemState`'s vocabulary should be dumped statically with
|
||||
`fifa17-recon/tools/vocab_dump.py` (already validated against the tradeState table)
|
||||
rather than guessed.
|
||||
|
||||
---
|
||||
|
||||
## Q2 — RETRACTED, and CLOSED as unrepresentable (2026-08-17, RE of the FUT front-end)
|
||||
|
||||
**Everything in the two Q2 sections above is superseded.** The `tradeState:"inactive"`
|
||||
projection has been removed from `GET …/tradePile` and `…/trade/status`, and the
|
||||
`UnlistedCandidate` machinery deleted. Live after the fix: `total` 7 → 1, only the real
|
||||
auction, `inactive` rows 0, coins unchanged, and all six former rows still present in
|
||||
`/club` (invariant intact).
|
||||
|
||||
### Why the earlier conclusion was wrong
|
||||
|
||||
It was `LIVE-CONFIRMED` that the row **renders**. That was never in doubt, and it is
|
||||
not the same claim as the row being usable. The rows were never actionable, and could
|
||||
not have been:
|
||||
|
||||
| Fact | Confidence |
|
||||
|---|---|
|
||||
| `tradeState` reaches ONE native branch, `cmp …,0x4` at `0x18013e619` ("is it `closed`?"). `inactive`(2) and `expired`(3) take the same edge → bit-identical `flagA`/`flagB`. Exhaustive 22-site census of `[reg+0x88]` reads across the PE. | CONFIRMED |
|
||||
| `tradeState` is handed to the movie verbatim as the Flash property `STATE` (publisher `0x1801bf030`); the action gate is in the APT/ActionScript FUT front-end, not CardsDLL. | CONFIRMED |
|
||||
| The trade-pile movie class partitions rows with `getCardsInAuction`/`isInActiveAuction` (traces `initPile() - IN AUCTION:` / `- NOT IN AUCTION:`), and only auction rows reach `PreCheckCardOptions` → `handleTradeCardAction`. | CONFIRMED (interned AS symbols + trace strings, live) |
|
||||
| A client rule excludes non-auction pile rows from the action path (Category C). | STRONGLY SUPPORTED |
|
||||
| The gate term is `STATE == 2`. | **NOT PROVEN** — `STATE`, `CARD_OFFERSTATE` and the `RESERVEDPRICE`/`MAX_CREDITS` pair all differ together in every row we can observe |
|
||||
| In the shipped game, unlisted pile rows were a CLIENT-SIDE transient built by the movie from a `TO_TRADEPILE` Flash message (`FUN_1801be6a0`), never server-delivered. | STRONGLY SUPPORTED |
|
||||
|
||||
This fires the corpus's own pre-registered falsifier E3
|
||||
(`plan-2026-08-06-transfer-market.md:368-373`): the native predicates all pass
|
||||
(`IS_TRADING_ENABLED=1`, `TRADE_PILE_SIZE=100`, `tradeable=1`) and an `expired` row in
|
||||
the same section IS actionable, so "the Flash layer has its own gate and CardsDLL is no
|
||||
longer the right place to look" is now an observation.
|
||||
|
||||
**The corpus said so before any of this was written**, at
|
||||
`plan-2026-08-06-transfer-market.md:731-733`: "`inactive` decodes but no client path
|
||||
treats it specially; **do not emit it**." The projection was built anyway, and the
|
||||
earlier reasoning above (that the warning "was written about the PRESENTATION
|
||||
function") was motivated rather than evidenced.
|
||||
|
||||
**Consequence for the product:** a server cannot put an unlisted item on the Transfer
|
||||
List in an actionable state. `/tradePile` carries only twelve-atom auction records and
|
||||
`pile` has no deserializer arm, so anything rendered there must be a real auction.
|
||||
Unlisted trade-pile membership is now internal state with no wire expression; those
|
||||
cards live in `/club`, which is where the client can act on them.
|
||||
|
||||
### Open: `itemData.itemState: "listFS"` is not a FIFA 17 token
|
||||
|
||||
Independent of the gate, and CONFIRMED: `listFS` occurs **0 times** in
|
||||
`CardsDLL_Win64_retail.dll` (md5 `4de3493131d7d2ff7f8b360c5ac9b655`) and 0 times in
|
||||
4.26 GiB of live process memory; it decodes to **−1** through `FUN_180166660`, observed
|
||||
live on our listed row. FIFA 17's value for a listed item is `forSale` (5), from the
|
||||
12-row table at `0x180229cc0`. We emit `listFS` on `/tradePile` and `/trade/status`, and
|
||||
so does the Python oracle (`utas_server.py:3921`) — which is exactly why the
|
||||
differential never caught it: **oracle parity is necessary but not sufficient.**
|
||||
|
||||
### Fix B — `forSale` PROMOTED, live-confirmed on the expired path (2026-08-17)
|
||||
|
||||
Shipped alone, then validated by an operator FIFA 17 session against the already-expired
|
||||
row `1000000155` (res 158023, 93 RW).
|
||||
|
||||
| Claim | Status |
|
||||
|---|---|
|
||||
| `listFS` is not a FIFA 17 token and decodes to −1 | **CONFIRMED** (binary + 4.26 GiB process scan + live row) |
|
||||
| `forSale` is FIFA 17-native, value 5 | **CONFIRMED** (12-row table at `0x180229cc0`) |
|
||||
| An expired row carrying `forSale` is still ACTIONABLE, and Return to Club works | **LIVE-CONFIRMED** — operator returned the card; server verified below |
|
||||
| The result is durable (a fresh session reconstructs it) | **LIVE-CONFIRMED** (Gap 2, 2026-08-17) — the operator exited FUT and re-entered; the host log shows `auth-delete` then two fresh `sid_opened=true` sessions, each re-fetching `/club` and `/tradePile`, and the card was still in My Club. 18/18 server checks passed identically before and after. See the Gap 2 closure section. |
|
||||
| An ACTIVE seller row carrying `forSale` renders correctly | **LIVE-CONFIRMED** (Gap 1, 2026-08-17) — see the closure section below; the client decoded `CARD_OFFERSTATE = 5` out of its own auction record |
|
||||
| `CARD_OFFERSTATE` is the Flash action-gate term | **STILL UNPROVEN** — this change does not test it, and the result does not bear on it |
|
||||
|
||||
What this rules out: `CARD_OFFERSTATE` is not a gate term that *requires* −1. An expired
|
||||
row decoding `CARD_OFFERSTATE = 5` remained actionable, so the earlier worry — that every
|
||||
actionable row we had ever seen carried `itemState = −1` — was a coincidence of our own
|
||||
bad token, not a client rule. It narrows nothing else: `STATE` and the
|
||||
`RESERVEDPRICE`/`MAX_CREDITS` pair are untouched and still confounded with each other.
|
||||
|
||||
Wire A/B (`docs/evidence/market-lifecycle-2026-08-17/itemstate-forsale-ab.json`): on the
|
||||
same expired row, **exactly one field changed** — `itemState` `listFS` → `forSale`.
|
||||
`tradeId`, `tradeState`, `expires`, `startingBid`, `buyNowPrice`, `currentBid`,
|
||||
`bidState`, `sellerName`, `sellerEstablished`, `watched`, `coinsProcessed`, the atom
|
||||
count (12), and the whole `itemData` card are byte-identical; coins unchanged; Fix A's
|
||||
zero `inactive` rows intact.
|
||||
|
||||
Risk was lower than it first appeared, for a reason worth recording: the market SEARCH
|
||||
route (`/transfermarket`) **already** emitted `forSale`, so the client has been decoding
|
||||
`forSale` (5) on a live path all along — only the seller's own pile carried the invalid
|
||||
token.
|
||||
|
||||
Differential consequence: `market query tradePile` moves from `PARITY` to
|
||||
`DIFFERENT-BY-DESIGN`, pinning `oracle == "listFS"` and `rust == "forSale"` so the
|
||||
divergence stays visible. That assertion previously demanded parity and passed while
|
||||
**both** sides were wrong — the mechanism by which this defect survived every
|
||||
differential run. **Where the FIFA 17 binary contradicts the Python oracle, the binary
|
||||
wins.**
|
||||
|
||||
Revert is one line if the live A/B fails: both `auction_record_as(l, "forSale")` calls in
|
||||
`market.rs` back to `"listFS"`, rebuild, restart `prod-host`. On failure the rule is to
|
||||
revert and go to the AVM1 gate work — NOT to compensate with another field change.
|
||||
|
||||
**Claim boundary:** this is protocol correctness of one token. It is not evidence about
|
||||
the Flash decision tree; `STATE`, `CARD_OFFERSTATE` and `RESERVEDPRICE`/`MAX_CREDITS`
|
||||
remain confounded.
|
||||
|
||||
### Gap 1 CLOSED — an ACTIVE seller row under `forSale` (2026-08-17)
|
||||
|
||||
The one thing the Fix B promotion could not test, because no active listing existed
|
||||
during that session. Closed with a deliberately disposable card: a **75 ST**, resourceId
|
||||
`212188`, wire id `100000178` — one of **three identical owned copies**, not in the
|
||||
11-man squad, `untradeable: false`, discardValue 600. Listed **through the real FIFA 17
|
||||
client** at Start 150 / Buy Now 200 for the shortest offered duration (1h), so expiry
|
||||
would arrive naturally. No timestamp was touched.
|
||||
|
||||
Listing creation was entirely Rust-owned:
|
||||
`PUT …/item` (club → pile) → `route=market-list POST item_id=100000178 listed=true
|
||||
trade_id=1000000178` → `POST …/auctionhouse 200`, then the client polling
|
||||
`GET …/trade/status?tradeIds=1000000178` (`requested=1 returned=1`) for the row it draws.
|
||||
|
||||
Wire, active row `1000000178`
|
||||
(`docs/evidence/market-lifecycle-2026-08-17/gap1-active-forsale.json`):
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| `itemState` | **`forSale`** |
|
||||
| `tradeState` | `active` |
|
||||
| `startingBid` / `buyNowPrice` / `currentBid` | 150 / 200 / 0 |
|
||||
| `bidState` | `none` |
|
||||
| `expires` | **3562 → 3556** over a 6-second sample — a live clock, not a frozen scalar |
|
||||
| atom count | **12**, exactly what `0x18013e410` reads |
|
||||
| `itemData` | full card, 6-entry `attributeList` |
|
||||
| `/tradePile/counts` | `count 1, selling 1, offered 0, sold 0` |
|
||||
| coins | 29,843,976 unchanged |
|
||||
| Fix A | 0 `inactive` rows |
|
||||
|
||||
`/trade/status` stayed coherent with `/tradePile` throughout, agreeing on tradeId,
|
||||
`active`, `forSale` and a decreasing `expires`.
|
||||
|
||||
**The decisive observation** is not the wire — it is what the client made of it. A
|
||||
read-only `/proc/<pid>/mem` decode of the live trade-pile auction record (slide proven by
|
||||
FNV control MATCH first; `O_RDONLY` + `pread` only) returned:
|
||||
|
||||
```text
|
||||
tradeId=1000000178 state=1(active) bid=0(none) buyNow=200 start=150 cur=0 expires=3457
|
||||
item CARD_ID=212188 cardType=1 tradeable=1 itemState=5(forSale) pile=0 rating=75
|
||||
```
|
||||
|
||||
`itemState = 5(forSale)`. The same field read `-1(<unrecognised>)` under `listFS`. This is
|
||||
the direct A/B on the only changed field, taken from the client's own memory rather than
|
||||
from our response — the strongest form of this evidence available without AVM1 work.
|
||||
`pile = 0` again, confirming pile is still never set from the wire.
|
||||
|
||||
Operator UI acceptance: card under **LISTED ITEMS** (not the TRANSFER LIST section), Start
|
||||
150 / Buy Now 200 correct, **Time Remaining visibly counting down**, normal card art, no
|
||||
malformed row, no duplicate, no premature Expired, no crash, and correctly
|
||||
**non-actionable while active**. The client's container populating with exactly this row
|
||||
(`rows=1`) independently corroborates that the Transfer List screen bound and drew it.
|
||||
|
||||
**No code change was required** — the promoted implementation was already correct on the
|
||||
active path, which is what the test was designed to find out.
|
||||
|
||||
Two process notes worth keeping. First, an early capture reported "countdown NO"; that was
|
||||
a bug in the capture script (it re-read a hardcoded copy id rather than the row it had
|
||||
resolved), not a server fault — the same run's `/trade/status` already showed a lower
|
||||
`expires` than `/tradePile`. Verify the instrument before believing a negative. Second,
|
||||
the client's auction-record containers read **0 rows** while the operator was on any other
|
||||
screen; they populate only while the Transfer List is bound. An empty container is not
|
||||
evidence of an empty pile.
|
||||
|
||||
**Claim boundary unchanged:** this proves the client *decodes* the token. It says nothing
|
||||
about the Flash action-gate term.
|
||||
|
||||
|
||||
### Proving the gate (not yet done)
|
||||
|
||||
Disassemble the AVM1 action buffer of `tradepile.isInActiveAuction` (and
|
||||
`PreCheckCardOptions`): either walk from the interned symbol at `0xb84d7498` to its APT
|
||||
function object in the live process, or extract
|
||||
`external/ion_fut/screens/trading/tradepile` from `ui.sb` + the `cas` archives offline.
|
||||
Nothing else in reach separates `STATE` from `CARD_OFFERSTATE` from the price pair,
|
||||
because all three differ together in every observable row.
|
||||
|
||||
---
|
||||
|
||||
## Gap 2 CLOSED — Return to Club survives a full exit/re-entry (2026-08-17)
|
||||
|
||||
The claim the Fix B promotion could only make server-side. Closed on the same
|
||||
disposable card as Gap 1 (75 ST, res `212188`, wire id `100000178`, tradeId
|
||||
`1000000178`) after it expired **naturally** — the 1h auction was allowed to run
|
||||
out. No timestamp was mutated at any point in either gap.
|
||||
|
||||
### The whole lifecycle, observed rather than forced
|
||||
|
||||
A read-only sampler polled `/tradePile` once a minute for the auction's full hour
|
||||
(55 samples). `expires` is derived from `created_at + duration`, so watching is the
|
||||
only honest way to see expiry happen:
|
||||
|
||||
```text
|
||||
t=1787012091 active expires 3175 itemState forSale
|
||||
... counting down, one sample a minute ...
|
||||
t=1787015271 expired expires 0 itemState forSale <- natural expiry
|
||||
t=1787015301 (absent) total 0 <- Return to Club
|
||||
```
|
||||
|
||||
`itemState` stayed `forSale` across the `active -> expired` transition, which is the
|
||||
Fix B behaviour under the one state change we had never watched live.
|
||||
|
||||
### Return to Club, then two session boundaries
|
||||
|
||||
The host log records the coupled transition and the re-entries:
|
||||
|
||||
```text
|
||||
route=move-items wire=100000178 pile=club auction_cancelled=1
|
||||
route=auth-delete
|
||||
route=auth ... ip=10.10.0.105 persona=33068179 sid_opened=true
|
||||
route=hub clubPlayers=1966 auctionCount=0
|
||||
route=club total=1986 emitted=1966 dropped_no_asset=0
|
||||
... the full auth -> userMassInfo -> hub -> club -> tradePile -> trade/status
|
||||
cycle then repeats for a SECOND fresh session ...
|
||||
```
|
||||
|
||||
`auction_cancelled=1` is `cancel_active_for_core_item` firing: the pile move to
|
||||
`club` ends the auction that put the card there, which is the coupled transition
|
||||
that keeps pile membership and auction lifecycle from disagreeing.
|
||||
|
||||
Two independent fresh sessions each rebuilt the state from durable storage, and the
|
||||
operator confirmed the card was still in My Club. One session boundary was the
|
||||
requirement; two is stronger.
|
||||
|
||||
### Server state — 18/18, byte-identical before and after re-entry
|
||||
|
||||
| Surface | Result |
|
||||
|---|---|
|
||||
| `/tradePile` | `total 0`, 0 rows, no reference to the item or the tradeId |
|
||||
| `/tradePile/counts` | `count 0, selling 0, sold 0, offered 0` |
|
||||
| `/trade/status` | tradeId `1000000178` → **`closed`**, `expires 0` — still resolves, correctly terminal |
|
||||
| `/club` | **1966** items; `id=100000178 res=212188 rating=75 ST itemState=free untradeable=False` |
|
||||
| Duplicate owned ids | **0** across all 1966 |
|
||||
| Market store | listing `1000000178` = `cancelled`; **0 active, 0 reserved** anywhere |
|
||||
| Coins | **29,843,976** unchanged for the entire test |
|
||||
| Fix A | 0 `inactive` rows |
|
||||
|
||||
The `1965 → 1966` club count is the pile-aware exclusion working in both directions:
|
||||
the card left `/club` while actively listed and returned on cancel. A tradeId that
|
||||
can still be asked about still answers — terminally — rather than vanishing.
|
||||
|
||||
**No code change was required for either gap.** Both tests were designed to find out
|
||||
whether the promoted implementation was already correct on paths it had not been
|
||||
exercised on. It was.
|
||||
|
||||
---
|
||||
|
||||
## FROZEN BASELINE — the known-good transfer-list lifecycle (2026-08-17)
|
||||
|
||||
With both gaps closed, this lifecycle is the reference. A future change that alters
|
||||
any line here is a regression until proven otherwise.
|
||||
|
||||
```text
|
||||
CLUB
|
||||
itemState free, present in /club, absent from /tradePile
|
||||
| PUT /item {pile:"trade"} + POST /auctionhouse (ISStart)
|
||||
v
|
||||
ACTIVE AUCTION
|
||||
/tradePile: tradeState active, itemState forSale, 12 atoms,
|
||||
expires counting down from duration, currentBid 0, bidState none
|
||||
/tradePile/counts: count 1, selling 1
|
||||
/club: EXCLUDED while listed
|
||||
/trade/status: agrees with the pile on every field
|
||||
client: renders under LISTED ITEMS, CARD_OFFERSTATE = 5, non-actionable
|
||||
| wall clock reaches created_at + duration
|
||||
v
|
||||
EXPIRED
|
||||
/tradePile: tradeState expired, expires 0, itemState still forSale
|
||||
client: row becomes ACTIONABLE, offers Return to Club
|
||||
| PUT /item {pile:"club"} -> cancel_active_for_core_item
|
||||
v
|
||||
CLUB (terminal)
|
||||
/tradePile: total 0 /tradePile/counts: all zero
|
||||
/club: present again, itemState free market store: cancelled
|
||||
/trade/status: old tradeId -> closed, expires 0
|
||||
coins: unchanged throughout
|
||||
survives: FUT exit + re-entry, repeatedly
|
||||
```
|
||||
|
||||
Invariants this baseline pins:
|
||||
|
||||
* an item is never in limbo — it is in `/club` or in an active auction, never neither;
|
||||
* `expires` is SECONDS REMAINING and must evolve with wall-clock;
|
||||
* pile membership and auction lifecycle are separate facts that must be changed
|
||||
together;
|
||||
* a tradeId resolves on every route that can be asked about it, even when terminal;
|
||||
* only real active auctions appear in `/tradePile` (Fix A);
|
||||
* a listed item carries `itemState forSale` (Fix B), and the client decodes 5;
|
||||
* an active own auction is intentionally non-actionable;
|
||||
* coins move only when coins should move — this whole lifecycle moves none.
|
||||
|
||||
What is still NOT established: the SOLD path (no counterparty exists yet; settlement
|
||||
is implemented and unit-tested in Core but never client-validated — see
|
||||
`MARKET_SOLD_SETTLEMENT.md`), `/tradePile/counts` exact FIFA semantics, and the AVM1
|
||||
action gate.
|
||||
|
||||
@@ -1,352 +0,0 @@
|
||||
# Transfer-market sold settlement + transaction fee
|
||||
|
||||
How a completed market sale moves coins and ownership. Written while the
|
||||
`forSale` expiry lifecycle test was running, so **nothing here has been deployed
|
||||
or validated against a FIFA client** — see the confidence table.
|
||||
|
||||
Every claim carries one of:
|
||||
|
||||
| Tag | Meaning |
|
||||
|---|---|
|
||||
| **CODE** | Read directly out of the current implementation |
|
||||
| **TESTED** | Pinned by a unit or integration test that fails if it regresses |
|
||||
| **FIFA17-HISTORICAL** | Supported by contemporaneous FIFA 17 knowledge, not by our client binary |
|
||||
| **NEEDS-LIVE** | Requires a real FIFA 17 client to confirm; must not be treated as settled |
|
||||
|
||||
---
|
||||
|
||||
## 1. What the Buy Now path did before this work
|
||||
|
||||
Traced end to end, **CODE**:
|
||||
|
||||
```text
|
||||
POST/PUT /ut/game/fifa17/trade/<id>
|
||||
-> openfut-utas-host/src/lib.rs route classification (EconomyRoute::MarketBuy)
|
||||
-> market.rs::handle_market_buy
|
||||
store.get_listing(id) market_store.rs
|
||||
bid < buy_now -> echo raised bid, NO coin movement, return
|
||||
store.reserve_listing(id) CAS active -> reserved
|
||||
econ.balance() pre-check, 461 if short
|
||||
econ.purchase_item(price, "market-buy:<id>", listing.card_id)
|
||||
-> POST {core}/economy/purchase-item
|
||||
-> openfut-core services/economy.rs::purchase_item
|
||||
BEGIN IMMEDIATE; debit(club, cost); add_item(club, item, card); COMMIT
|
||||
store.complete_sale(id) CAS reserved -> sold
|
||||
-> auctionInfo record with tradeState "closed", bidState "highest",
|
||||
currentBid = price, itemData.itemState "free"
|
||||
```
|
||||
|
||||
The module header at `openfut-utas-host/src/market.rs:12-16` states it plainly:
|
||||
a buy-now *"debits the buyer and MINTS the won card into their club … there is no
|
||||
real counterparty and no seller credit"*.
|
||||
|
||||
### The ten questions, answered from code
|
||||
|
||||
| # | Question | Answer (**CODE**) |
|
||||
|---|---|---|
|
||||
| 1 | Where are buyer coins debited? | `services/economy.rs::debit` inside `purchase_item`, one `BEGIN IMMEDIATE` transaction. Host pre-checks with `econ.balance()` only to distinguish 461 from 503. |
|
||||
| 2 | Does Core know the seller? | **No.** Every `/economy/*` route resolves ONE club from the `X-OpenFUT-Game` active profile (`routes/economy.rs:23-28`); no request shape carries a counterparty. |
|
||||
| 3 | Does the auction persist seller-owned item identity? | **Yes** — `listings.core_item_id` and `wire_item_id` (`market_store.rs:199-203`). The settlement inputs already existed; the buy path just ignored them. |
|
||||
| 4 | Transfer or mint? | **Mint.** `purchase_item` INSERTs a new `owned_cards` row keyed `market-buy:<tradeId>`. The seller's row is untouched, so a sold card existed **twice**. |
|
||||
| 5 | Is the seller credited? | **No.** Nowhere. |
|
||||
| 6 | Any tax calculation? | **No.** Zero occurrences of `*5/100`, `0.95`, `fee`, `tax` in Core, host, adapter *or* the Python oracle. It existed only as prose in `FIFA17_HUB_BEHAVIOR_SPEC.md:884` and `FIFA17_TRANSFER_MARKET_WIRE.md:127-131`, both labelled unimplemented. |
|
||||
| 7 | Auction state sequence? | `active -> reserved -> sold`, host-side (`market_store.rs`). On any Core failure `reserved -> active`. |
|
||||
| 8 | Is there a `reserved` state? | **Yes**, and it is the concurrency guard, not decoration — `reserve_listing` is a `BEGIN IMMEDIATE` compare-and-swap, so of two concurrent buyers exactly one proceeds to a debit. |
|
||||
| 9 | When does the item leave the seller's `/tradePile`? | When the listing stops being `active`: the pile projection emits only real active auctions (the Fix A invariant). `sold`/`cancelled` rows vanish from the pile the moment the CAS lands. |
|
||||
| 10 | What makes a retry safe? | The same CAS. A second Buy Now finds the listing non-`active`, so `reserve_listing` returns `false` and the handler returns an empty `auctionInfo` — the client reads "auction gone", and no economy call is made. |
|
||||
|
||||
### Deficiencies
|
||||
|
||||
1. **Ownership duplication.** A sold card ends up owned by both parties. Structural, not a race.
|
||||
2. **No seller credit** — selling was economically pointless.
|
||||
3. **No fee**, so no sink; every synthetic buy was pure coin destruction and every sale would have been pure creation.
|
||||
4. **Core could not express a counterparty at all** — the single-club resolver had no seller/buyer distinction to make.
|
||||
5. `UPDATE owned_cards SET club_id` did not exist anywhere in Core: there was **no ownership-transfer primitive** to call.
|
||||
|
||||
---
|
||||
|
||||
## 2. The settlement transaction
|
||||
|
||||
`openfut-core/src/services/economy.rs`, alongside the existing tx-scoped
|
||||
primitives so it reuses the module's proven atomicity (`pool.acquire()` +
|
||||
`BEGIN IMMEDIATE` + `finish()`), **CODE**:
|
||||
|
||||
```rust
|
||||
pub enum SaleBuyer<'a> { Club(&'a str), Outside }
|
||||
pub struct SaleTerms { pub gross: i64, pub fee: i64 }
|
||||
|
||||
pub async fn settle_sale(
|
||||
pool: &Pool, item_id: &str, seller_club_id: &str,
|
||||
buyer: SaleBuyer<'_>, terms: SaleTerms,
|
||||
) -> AppResult<SaleReceipt>
|
||||
```
|
||||
|
||||
One transaction: debit buyer `gross` → evict the item from every squad →
|
||||
transfer the existing row → credit seller `gross - fee`. The fee is simply never
|
||||
credited anywhere, which is what destroys it.
|
||||
|
||||
Exposed as `POST /economy/settle-sale`. It is the **only** economy route that
|
||||
names clubs explicitly, because a sale has two sides and the active-profile
|
||||
resolver can only describe one. Both are optional: an omitted `seller_club_id`
|
||||
means the active club (the production shape — the player listed the item), and an
|
||||
omitted `buyer_club_id` means `Outside`, *not* the active club, so the route can
|
||||
never accidentally settle a club against itself.
|
||||
|
||||
### Ownership transfer, not duplication
|
||||
|
||||
```sql
|
||||
UPDATE owned_cards SET club_id = ? WHERE id = ? AND club_id = ?
|
||||
```
|
||||
|
||||
No INSERT and no DELETE on the two-party path, so the instance id, its
|
||||
`chemistry_style`/`position_override`/`training_bonus` and its `acquired_at` all
|
||||
survive and the inventory row count cannot change. Duplication is ruled out
|
||||
structurally rather than by an assertion. `acquired_at` is deliberately preserved:
|
||||
this is the same instance under new ownership, not a new acquisition.
|
||||
|
||||
**TESTED** — `settle_sale_transfers_ownership_and_splits_coins` asserts both that
|
||||
the item's owner changed and that `SELECT COUNT(*) FROM owned_cards` is still 1.
|
||||
|
||||
### The seller is pinned, not inferred — and this was a real bug
|
||||
|
||||
The first implementation derived the seller from *current* ownership, which reads
|
||||
naturally ("the seller is whoever owns it") and is wrong. The concurrency test
|
||||
caught it immediately: **both** racing buyers succeeded, because after the first
|
||||
sale the item was owned by buyer B, so the second call read B as the seller and
|
||||
cheerfully chain-sold it B → C. Core has no listing concept, so it had no way to
|
||||
notice that this was a replay of one listing rather than a new sale.
|
||||
|
||||
The fix is that `seller_club_id` is the club the caller **believes** owns the item,
|
||||
and every ownership statement is predicated on it. That turns the `UPDATE` into an
|
||||
ownership compare-and-swap which is authoritative about "this sale already
|
||||
happened", independently of any caller-side listing state. Safety is not weakened:
|
||||
a caller still cannot credit a club that never owned the item, because the credit
|
||||
only follows a matched CAS.
|
||||
|
||||
**TESTED** — `two_buyers_racing_one_item_settle_once` (exactly one winner, loser's
|
||||
balance untouched, one instance, seller paid once) and
|
||||
`settling_the_same_sale_twice_pays_once`.
|
||||
|
||||
### Squad eviction is mandatory
|
||||
|
||||
`squad_players.owned_card_id` is a foreign key onto `owned_cards(id)` and the pool
|
||||
enables `foreign_keys`. Two consequences, both **CODE**:
|
||||
|
||||
* an `Outside` sale of a squadded card would fail outright on the FK;
|
||||
* a two-party transfer preserves the row id, so a surviving `squad_players` row
|
||||
would leave the **previous** owner fielding a card they no longer own.
|
||||
|
||||
So both paths evict first, and the receipt reports `squad_slots_freed`.
|
||||
**TESTED** — `sale_evicts_the_item_from_the_sellers_squad`.
|
||||
|
||||
> Noticed while reading, NOT fixed here (out of scope, no behaviour changed):
|
||||
> `routes/cards.rs:187-192` (quick sell) and `services/market.rs` both DELETE an
|
||||
> owned card without clearing `squad_players` and without a transaction, so
|
||||
> quick-selling a squadded card should fail on the same FK. Worth a follow-up.
|
||||
|
||||
---
|
||||
|
||||
## 3. The fee arithmetic
|
||||
|
||||
Lives in `openfut-adapter-fifa17/src/fut/economy_policy.rs`, beside `pack_price`
|
||||
and `match_reward_total`, because 5% is a **FIFA policy constant** and Core must
|
||||
stay game-neutral. Core validates `0 <= fee <= gross` and never computes a rate.
|
||||
|
||||
```rust
|
||||
pub const TRANSFER_MARKET_FEE_PERCENT: i64 = 5;
|
||||
pub fn transfer_market_fee(gross: i64) -> i64 { /* floor, i128 intermediate */ }
|
||||
pub fn seller_proceeds(gross: i64) -> i64 { gross.max(0) - transfer_market_fee(gross) }
|
||||
```
|
||||
|
||||
Integer only. Floating point is never used for coin settlement: `0.05` is not
|
||||
representable in binary, and at large prices a `f64` round trip can create or
|
||||
destroy a coin. The multiply widens to `i128`, so overflow is unreachable for any
|
||||
`i64` price and no price ceiling has to be assumed.
|
||||
|
||||
**Rounding is a CHOICE, and it is NEEDS-LIVE.** The fee is floored, so the seller
|
||||
keeps the fractional coin. That was chosen because it makes
|
||||
|
||||
```text
|
||||
fee + proceeds == gross
|
||||
```
|
||||
|
||||
hold exactly at every input, which is what the accounting invariant rests on. The
|
||||
discriminating case against the alternative (flooring the seller's 95% instead) is
|
||||
a gross of **150**: this rule pays **143**, the alternative pays **142**. Nothing
|
||||
in the corpus or the client binary settles which the real server did — the client
|
||||
is only ever told the gross, and no `tax`/`netPrice`/`sellerProceeds` wire field
|
||||
exists. **A future live test should list something at 150 coins and read the
|
||||
credited amount.**
|
||||
|
||||
**TESTED** at `0, 1, 19, 20, 21, 39, 40, 100, 150, 200, 1_000, 15_000,
|
||||
15_000_000, i64::MAX`, plus `fee + proceeds == gross` swept over 0..2000 and the
|
||||
extremes, plus negative-gross rejection.
|
||||
|
||||
| gross | fee | proceeds | why it is in the table |
|
||||
|---|---|---|---|
|
||||
| 19 | 0 | 19 | last fee-free price |
|
||||
| 20 | 1 | 19 | first price that pays |
|
||||
| 150 | 7 | 143 | **the NEEDS-LIVE discriminator** (alternative: 8 / 142) |
|
||||
| 15,000 | 750 | 14,250 | canonical fixture |
|
||||
| i64::MAX | MAX/20 | remainder | proves no overflow |
|
||||
|
||||
---
|
||||
|
||||
## 4. Accounting invariant
|
||||
|
||||
For `SaleBuyer::Club`, **TESTED** by `sale_conserves_coins_minus_the_fee`:
|
||||
|
||||
```text
|
||||
buyer_debit == seller_credit + fee
|
||||
total modelled coins shrink by EXACTLY the fee
|
||||
21,000 -> 20,250 (difference 750)
|
||||
```
|
||||
|
||||
For `SaleBuyer::Outside` the identity deliberately differs and the doc comment
|
||||
says so: the counterparty owns no `clubs` row, so nothing is debited and the
|
||||
seller's proceeds **enter** the economy from outside. **TESTED** by
|
||||
`settle_sale_to_outside_retires_the_item_and_pays_net`, which asserts total coins
|
||||
*rise* by the proceeds. Conflating the two would look like a conservation bug.
|
||||
|
||||
---
|
||||
|
||||
## 5. Rejected sales
|
||||
|
||||
All **TESTED** by `invalid_sales_change_nothing`, which re-seeds a fresh fixture
|
||||
per case and asserts both balances and the item's owner afterwards:
|
||||
|
||||
| Case | Outcome |
|
||||
|---|---|
|
||||
| buyer cannot afford `gross` | rejected, nothing moves |
|
||||
| buyer == seller | rejected at the Core domain boundary — not a market path, and it would otherwise debit and credit one club while destroying the fee |
|
||||
| item does not exist | rejected |
|
||||
| item not owned by the named seller (**includes every replay**) | rejected |
|
||||
| buyer club does not exist | rejected |
|
||||
| `fee > gross` | rejected |
|
||||
| negative fee / negative gross | rejected |
|
||||
| ownership changed mid-flight | rejected by the CAS |
|
||||
|
||||
A zero-price sale is **legal** (a free transfer, no fee) — **TESTED** by
|
||||
`zero_price_sale_is_a_free_transfer`.
|
||||
|
||||
Auction-level states (`expired`, `cancelled`, already-`sold`) are **not** Core's
|
||||
business: Core has no listing concept, and the host's `active -> reserved` CAS
|
||||
already refuses those. Duplicating that check in Core would create a second
|
||||
authority on auction lifecycle.
|
||||
|
||||
---
|
||||
|
||||
## 6. Atomicity
|
||||
|
||||
`services::economy` is the **only** module in Core that is genuinely atomic; it
|
||||
uses `pool.acquire()` + raw `BEGIN IMMEDIATE` + `finish()` deliberately, because a
|
||||
DEFERRED `pool.begin()` upgrades to a write at first write, where SQLite returns
|
||||
`SQLITE_BUSY` immediately instead of honouring `busy_timeout`
|
||||
(`services/economy.rs:218-223`). Settlement is built inside that module for
|
||||
exactly this reason, so it inherits the guarantee rather than re-deriving it.
|
||||
|
||||
Handlers receive a pooled clone, never `&mut SqliteConnection`, so the transaction
|
||||
boundary is inside the service function. **CODE.** No externally observable state
|
||||
exists where the buyer is debited without the item moving, or the seller credited
|
||||
without the buyer debited — every step shares one connection inside one
|
||||
transaction, and `finish()` rolls back the whole thing on any error.
|
||||
|
||||
### Rejection precedence, and a test that passed for the wrong reason
|
||||
|
||||
Inside one transaction the order of the steps cannot change the final state — any
|
||||
error rolls everything back. It does change **which reason** a rejection carries,
|
||||
and that turned out to matter.
|
||||
|
||||
The first implementation debited the buyer before touching ownership, on the
|
||||
reasonable-sounding grounds that an unaffordable sale should fail before ownership
|
||||
moves. The isolated staging harness then showed that a replayed settlement was
|
||||
refused with `insufficient balance: have 5000, need 15000` — because the buyer had
|
||||
already spent the coins on the sale that succeeded. The affordability guard fired
|
||||
first, so **the ownership CAS was never consulted**, and the unit test named
|
||||
`settling_the_same_sale_twice_pays_once` was passing without ever exercising the
|
||||
replay guard it claimed to test.
|
||||
|
||||
Two fixes, both **TESTED**:
|
||||
|
||||
* ownership is now judged before affordability, so a replay is reported as
|
||||
`item not owned by club` (404) — the true cause;
|
||||
* the replay test now asserts the error is specifically `AppError::NotFound`, and
|
||||
adds a deliberately CHEAP replay (gross 100 out of the buyer's remaining 5,000)
|
||||
that only ownership can possibly refuse.
|
||||
|
||||
The lesson generalises: a guard that is merely *shadowed* by an earlier guard is
|
||||
untested, and a green assertion on "the second call failed" says nothing about
|
||||
which mechanism failed it.
|
||||
|
||||
---
|
||||
|
||||
## 6b. Isolated staging harness
|
||||
|
||||
`scripts/settlement-staging.py` — stdlib-only, exercises the settlement against a
|
||||
**real Core over real HTTP** with zero production contact. It picks an ephemeral
|
||||
loopback port (production 8099/8199/18080 sit in a hard deny-list checked in three
|
||||
places), runs Core once to apply its own migrations, seeds the canonical two-party
|
||||
fixture by direct SQL, relaunches Core, then prints BEFORE / PURCHASE / AFTER with
|
||||
PASS-FAIL lines and cleans up in a `finally`.
|
||||
|
||||
Result: **31/31 checks pass**, exit 0.
|
||||
|
||||
```text
|
||||
BEFORE seller 1,000 buyer 20,000 owner club-seller rows 1 total 21,000
|
||||
POST /economy/settle-sale {item_id, gross 15000, fee 750, seller, buyer} -> 200
|
||||
AFTER seller 15,250 buyer 5,000 owner club-buyer rows 1 total 20,250
|
||||
fee destroyed 750 duplicates 1 coins destroyed 750
|
||||
RETRY (identical) -> 404 item not owned by club: item-x nothing moved
|
||||
REPLAY GUARD (gross 100) -> 404 item not owned by club: item-x nothing moved
|
||||
```
|
||||
|
||||
Two things it found that the unit tests had not:
|
||||
|
||||
1. the rejection-precedence bug above;
|
||||
2. Core's **content preflight** aborts startup when an owned card references a
|
||||
`CardDefinitionId` that no loaded pack defines (`content preflight failed: 1
|
||||
owned card(s) reference CardDefinitionId(s) not loaded (e.g. ["def-x"])`), so
|
||||
the harness writes a one-entry content pack for its synthetic card rather than
|
||||
quietly substituting a stock card id.
|
||||
|
||||
---
|
||||
|
||||
## 7. What is NOT done, and why
|
||||
|
||||
* **No production wire change, no deployment.** The host gained the *capability*
|
||||
(`CoreEconomy::settle_sale`) but `handle_market_buy` is untouched: the synthetic
|
||||
buy path (`core_item_id == None`) has no counterparty, so minting there is
|
||||
correct and unchanged.
|
||||
* **Nothing decides that a player's listing has sold.** That trigger is the real
|
||||
remaining feature, and it needs the seller-facing sold wire representation,
|
||||
which is deliberately deferred — `tradeState`/`itemState` for a sold row are
|
||||
**NEEDS-LIVE** and must not be guessed. `docs/FIFA17_TRANSFER_MARKET_WIRE.md`
|
||||
records that no `sold` token exists in FIFA 17's `tradeState` table
|
||||
(`active=1 inactive=2 expired=3 closed=4`).
|
||||
* **`/tradePile/counts` semantics unchanged.** Its `sold` field stays as-is; the
|
||||
Core facts a correct `sold` count would need now exist, but the route output
|
||||
must not change without client evidence.
|
||||
* **The 5% rate itself is FIFA17-HISTORICAL**, and the rounding rule is
|
||||
**NEEDS-LIVE** (the 150-coin discriminator above).
|
||||
|
||||
## 8. Confidence summary
|
||||
|
||||
| Claim | Status |
|
||||
|---|---|
|
||||
| The old buy path minted a duplicate and never credited a seller | **CODE** |
|
||||
| No fee arithmetic existed anywhere in the project | **CODE** |
|
||||
| Ownership now transfers as one row, with no duplicate possible | **TESTED** |
|
||||
| Buyer debited, seller credited net, fee destroyed, in one transaction | **TESTED** |
|
||||
| `buyer_debit == seller_credit + fee` | **TESTED** |
|
||||
| Canonical 15,000 fixture: 20,000→5,000 / 1,000→15,250 / fee 750 | **TESTED** |
|
||||
| A replayed settlement pays once | **TESTED** |
|
||||
| Two racing buyers settle exactly once | **TESTED** |
|
||||
| A sold card leaves the seller's lineup | **TESTED** |
|
||||
| Invalid sales leave the economy unchanged | **TESTED** |
|
||||
| The fee rate is 5% of gross | **FIFA17-HISTORICAL** |
|
||||
| The fee is floored (150 → 143, not 142) | **UNDECIDABLE from available evidence** — CardsDLL contains no fee arithmetic, no `0.95`/`0.05` constant and no tax/net caption, so the client never computes or displays a net and there is no oracle to measure against. See `FIFA17_SOLD_WIRE_RE.md` §9. Remains a pinned CHOICE. |
|
||||
| The seller-facing sold wire state | **PARTIALLY RECOVERED** from the binary — see `FIFA17_SOLD_WIRE_RE.md`. Proven: no `sold` token in either vocabulary (exhaustive); `coinsProcessed` is published to Flash as `COINS_AWARDED`; a bulk clear-sold verb `DELETE …/trade/sold` exists; the seller's SOLD counter is real (atom `sold` 0x2c9 → `FUT_TF_SOLD`). Still UNDECIDABLE statically: `highest` vs `buyNow`. |
|
||||
| The client renders a settled sale correctly | **NEEDS-LIVE** — never exercised |
|
||||
|
||||
No Core test here establishes anything about the FIFA 17 wire contract. They
|
||||
establish the domain transaction only.
|
||||
@@ -1,208 +0,0 @@
|
||||
# FIFA 17 FUT Match Lifecycle
|
||||
|
||||
Design + contract reference for the **FUT match loop** as OpenFUT implements it on
|
||||
the backend/responder side. Consolidates knowledge previously scattered across
|
||||
`docs/PROJECT_STATE.md`, `fifa17-recon/tools/utas_server.py` (`match_route`),
|
||||
`openfut-utas-host` (Rust economy END leg), `fifa17-recon/tools/test_match_lifecycle.py`,
|
||||
and the vault (`Protocol Findings.md`, `Project State.md`, `Known Issues.md`).
|
||||
|
||||
Evidence labels: **OBSERVED** (live), **PROVEN** (test/static-analysis),
|
||||
**HYPOTHESIS** (reasoned, not yet live-confirmed).
|
||||
|
||||
> ## Status caveat (read first)
|
||||
> **No football match has ever started or completed in FIFA against this stack.**
|
||||
> The lifecycle below is validated by CardsDLL static analysis (RPC descriptor
|
||||
> blocks) + isolated persistence replay (`test_match_lifecycle.py`), **not** in-game
|
||||
> acceptance. The reward amounts are FUT-plausible env-tunable defaults, **not**
|
||||
> reversed values. Two blockers keep `FUT_MODES` **off by default** (see
|
||||
> [Open questions](#open-questions--blockers)).
|
||||
|
||||
## Scope
|
||||
|
||||
This documents the **UTAS responder handshake + economy reward** for a match — the
|
||||
HTTP calls CardsDLL makes around a match and the state they mutate. It does **not**
|
||||
cover the actual football simulation (Blaze game-server side, "past the FUT hub"),
|
||||
which remains unverified.
|
||||
|
||||
## The loop: a four-call state machine
|
||||
|
||||
CardsDLL issues six match RPCs; four form the playable loop. All share the base
|
||||
path `ut/<sku>/match`; the operation is discriminated by **suffix + body**, not by
|
||||
HTTP verb (verb selection lives outside CardsDLL, so the classifier is verb-agnostic).
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Created: POST /match (no matchId)
|
||||
Created --> Ready: POST|PUT /match/ready {matchId}
|
||||
Ready --> Playing: POST /match {matchId} (bare path + int matchId)
|
||||
Playing --> Ended: POST|PUT|DELETE /match/end {matchId, endReason, ...}
|
||||
Ended --> [*]: rewards credited, W/D/L + matchesPlayed persisted
|
||||
```
|
||||
|
||||
### Call classifier (`_match_call`, utas_server.py:3325)
|
||||
|
||||
The discriminator (**PROVEN** from CardsDLL RPC descriptors):
|
||||
|
||||
1. path ends `/match/end` → **END** (routed as `FutDestroyMatch` regardless of verb).
|
||||
2. body has integer `matchId` on the **bare** `/match` path → **PLAY** (`FutPlayGame`).
|
||||
CREATE and PLAY share `ut/<sku>/match`; the presence of an int `matchId` is the
|
||||
only discriminator — this is why a bare `/match` carrying `matchId` must NOT
|
||||
allocate a new match.
|
||||
3. path ends `/match/ready` → **READY** (`FutMatchReady`).
|
||||
4. otherwise → **CREATE** (`FutCreateMatch`).
|
||||
|
||||
## Per-call contracts
|
||||
|
||||
### CREATE — `POST /ut/<sku>/match` (empty/no `matchId`)
|
||||
CardsDLL: `FutCreateMatch` @ `0x180120380`; deserializes `startDateTime`(740,int),
|
||||
`reportIdEnabled`(641,bool). `squad`(717,nested) is a **FREEZE-RISK** and is omitted
|
||||
(SKIP-safe).
|
||||
|
||||
Response (`match_route`, utas_server.py:3399):
|
||||
```json
|
||||
{"startDateTime": <unix_ts:int>, "reportIdEnabled": false, "id": <matchId:int>}
|
||||
```
|
||||
Effect: allocates a match id; **advances `nextItemId` by 1** (PROVEN,
|
||||
`test_match_lifecycle.py:51`).
|
||||
|
||||
### READY — `POST|PUT /ut/<sku>/match/ready` (`{matchId}`)
|
||||
CardsDLL: `FutMatchReady` — no deserializer at all on the request; the server
|
||||
response parser has two scalar members + one nested member.
|
||||
|
||||
Response (`match_ready_body`, utas_server.py:3353):
|
||||
```json
|
||||
{"matchId": <int>, "opponentPersonaId": <int, default 0>}
|
||||
```
|
||||
- `opponentPersonaId` defaults to `0` — a neutral placeholder, **never** the
|
||||
logged-in user's persona.
|
||||
- The parser also has a nested `items` member (the opponent squad). It is **omitted
|
||||
deliberately** until the opponent-squad item contract is recovered from a live
|
||||
capture; unrecognized/absent members are skip-safe. **This omission is one of the
|
||||
two blockers.**
|
||||
|
||||
### PLAY — `POST /ut/<sku>/match` (bare path, `{matchId:int}`)
|
||||
CardsDLL: `FutPlayGame` — no request deserializer.
|
||||
|
||||
Response: `{}` (empty). Effect: **none** — must NOT allocate a match or advance
|
||||
`nextItemId` (PROVEN, `test_match_lifecycle.py:62-63`). This is purely a client
|
||||
keepalive/transition ack.
|
||||
|
||||
### END — `POST|PUT|DELETE /ut/<sku>/match/end` (`{matchId, endReason, myMatchStats, opponentMatchStats}`)
|
||||
CardsDLL: `FutDestroyMatch` @ `0x180121b60` — **the rewards call**. Routed as
|
||||
DestroyMatch regardless of verb (`FUT_MATCH_END`, default ON).
|
||||
|
||||
> **Wire path (Rust vs Python).** The Rust-owned reward is classified by `classify_economy`
|
||||
> on **`POST /ut/delete/game/<sku>/match`** — EA/CardsDLL tunnels DELETE-semantics ops through the
|
||||
> `/ut/delete/game/` prefix (`FutDestroyMatch` = `DELETE ut/%s/match/{id}`). Python's `match_route`
|
||||
> additionally accepts `/ut/game/<sku>/match/end`. Which exact form the retail client emits is
|
||||
> unverified (no match ever played); the Rust economy owns the `/ut/delete/game` form, and a
|
||||
> `/ut/game/.../match/end` would fall to Python. Both credit the same reward shape.
|
||||
|
||||
Request fields that matter:
|
||||
- `endReason` (atom 260) — **STRING enum, the AUTHORITATIVE result signal**. A score
|
||||
comparison is NOT how the client reports the outcome. Nine values, mapped to a
|
||||
win/draw/loss bucket:
|
||||
|
||||
| endReason | bucket |
|
||||
|---|---|
|
||||
| `WIN`, `DNF_WIN` | won |
|
||||
| `DRAW`, `DNF_DRAW`, `NO_CONTEST` | draw |
|
||||
| `LOSS`, `DNF_LOSS`, `DNF`, `QUIT` | loss |
|
||||
| (missing/unknown) | draw (neutral fallback — credits without inventing a win) |
|
||||
|
||||
- `myMatchStats` / `opponentMatchStats` — literal-keyed objects, 15 int fields each,
|
||||
first is `goals`. **Omitted by the client when `endReason` is `DNF`/`QUIT`**, so
|
||||
nothing may require them. Used only as a fallback outcome probe if `endReason` is
|
||||
absent.
|
||||
|
||||
Response (`destroy_match_body` / Rust `build_match_reward_body`) — every field a
|
||||
**top-level scalar** (zero freeze risk) except the deliberately nested reward:
|
||||
```json
|
||||
{
|
||||
"allCoins": <post-credit balance:int>,
|
||||
"matchCoins": <per-result coins:int>,
|
||||
"seasonCoins": 0,
|
||||
"tournamentCoins": 0,
|
||||
"boostConis": 0, // EA's typo — exact key required
|
||||
"participationAward": <int>,
|
||||
"teamOfTournamentWinner": false,
|
||||
"gameModeAward": { "coins": <total award:int> }
|
||||
}
|
||||
```
|
||||
> **Critical correction (2026-08-04):** the reward `coins` (atom 149) is read by the
|
||||
> deserializer **only inside `gameModeAward`**, never as a top-level key. An earlier
|
||||
> top-level `"coins"` was silently skipped and never reached the client — the one
|
||||
> field most obviously named "the reward" was the one going nowhere.
|
||||
|
||||
Effect (persisted to the active FUT save): credit coins; increment the matching
|
||||
`record.{won,draw,loss}`; increment `matchesPlayed` (PROVEN,
|
||||
`test_match_lifecycle.py:74-81`).
|
||||
|
||||
## Reward policy
|
||||
|
||||
Env-tunable defaults (FUT-plausible, **not reversed** — `economy_policy.rs:20-36`,
|
||||
`utas_server.py:3201-3206`):
|
||||
|
||||
| Result | Match coins | Participation | Total |
|
||||
|---|---|---|---|
|
||||
| Win | 400 (`FUT_MATCH_COINS_WIN`) | 0 (`FUT_MATCH_PARTICIPATION`) | 400 |
|
||||
| Draw | 200 (`FUT_MATCH_COINS_DRAW`) | 0 | 200 |
|
||||
| Loss | 100 (`FUT_MATCH_COINS_LOSS`) | 0 | 100 |
|
||||
|
||||
`allCoins` = post-credit balance; `gameModeAward.coins` = per-result + participation.
|
||||
|
||||
## Ownership split (Rust vs Python)
|
||||
|
||||
The match loop is **partially migrated**. Only the coin-crediting END leg is
|
||||
economy state, so only it is Rust/Core-owned; the CREATE/READY/PLAY legs are still
|
||||
served by the Python oracle.
|
||||
|
||||
| Call | Owner | Where |
|
||||
|---|---|---|
|
||||
| CREATE | Python | `utas_server.py::match_route` (via host `Route::Passthrough`) |
|
||||
| READY | Python | `utas_server.py::match_route` |
|
||||
| PLAY | Python | `utas_server.py::match_route` |
|
||||
| END (reward) | **Rust/Core** (on `POST /ut/delete/game/<sku>/match`) | `EconomyRoute::MatchEnd` → `handle_match_end` → `build_match_reward_body`; outcome via `economy_policy::match_result_from_reason`, coins via `match_result_coins`/`match_reward_total`. 503 on Core error, never Python. Python also accepts `/ut/game/<sku>/match/end`. |
|
||||
|
||||
END is classified in `classify_economy` (`openfut-utas-host/src/lib.rs`) and dispatched
|
||||
by the economy authority barrier ahead of the general classifier — so it can never
|
||||
also reach the Python passthrough (NEVER-BOTH). The Rust END writes the coin reward
|
||||
through the single Core economy transaction (`grant_reward`); W/D/L record + match
|
||||
count still live in the Python save until CREATE/READY/PLAY migrate.
|
||||
|
||||
## Test coverage
|
||||
|
||||
`fifa17-recon/tools/test_match_lifecycle.py` (PROVEN, isolated — temp profile, no
|
||||
live server): drives CREATE→READY→PLAY→END and asserts:
|
||||
- CREATE returns `reportIdEnabled:false` + advances `nextItemId` by 1.
|
||||
- READY returns exactly `{matchId, opponentPersonaId:0}`.
|
||||
- PLAY returns `{}` and does **not** advance `nextItemId`.
|
||||
- END credits `MATCH_COINS["won"] + MATCH_PARTICIPATION`, persists
|
||||
`record == {won:1,draw:0,loss:0}` and `matchesPlayed == 1`.
|
||||
|
||||
## Open questions / blockers
|
||||
|
||||
1. **READY `items` contract (opponent squad)** — the nested `items` member of
|
||||
`FutMatchReadyServerResponse` is unserved pending a live capture. Whether the
|
||||
client requires it present/non-empty to enter a match is unknown. **Blocker.**
|
||||
2. **Client mode-entry gate** — reaching a match from the FUT hub UI is unverified;
|
||||
`FUT_MODES` stays **off by default** (the season/tournament routes return `{}`).
|
||||
3. **No in-game acceptance** — every claim here is static-analysis + isolated replay.
|
||||
The first live match is expected to reveal whether the static read was complete
|
||||
(every match body is logged for exactly this reason).
|
||||
4. **Football simulation** — the actual gameplay (Blaze game-server) is out of scope
|
||||
here and unverified.
|
||||
|
||||
## Sources
|
||||
|
||||
- `docs/PROJECT_STATE.md` (match lifecycle status), `docs/direction.md` (Tier-2 loop).
|
||||
- `fifa17-recon/tools/utas_server.py`: `_match_call` (3325), `match_ready_body` (3346),
|
||||
`match_route` (3357), `destroy_match_body` (3281), `_match_result` (3236),
|
||||
`MATCH_COINS`/`MATCH_PARTICIPATION` (3201), `_END_REASON` (3229); CardsDLL RPC
|
||||
descriptor block (3183-3197).
|
||||
- `openfut-adapter-fifa17/src/fut/economy_policy.rs` (reward policy + outcome map).
|
||||
- `openfut-utas-host/src/lib.rs`: `EconomyRoute::MatchEnd`, `handle_match_end`,
|
||||
`build_match_reward_body`.
|
||||
- `fifa17-recon/tools/test_match_lifecycle.py` (lifecycle regression).
|
||||
- Vault: `02 Reverse Engineering/FIFA 17/Protocol Findings.md`,
|
||||
`06 Agent Memory/{Project State,Known Issues}.md`.
|
||||
@@ -1,165 +0,0 @@
|
||||
# Overnight session handoff — 2026-08-17
|
||||
|
||||
Autonomous session while you slept. Low-ceremony per your instruction. Everything
|
||||
below is verified as noted; nothing was committed or pushed (see
|
||||
[Uncommitted work](#uncommitted-work--needs-your-review)).
|
||||
|
||||
## TL;DR
|
||||
|
||||
1. **Production promotion completed** (you chose "promotion"): prod-host swapped to the
|
||||
post-P1 build, content-gap imported. Then I migrated the **remaining Python UTAS
|
||||
routes that have a known contract** to Rust: account/sync, ut/auth (SID mint),
|
||||
userMassInfo (full), clientdata, club/stats/{country,league,team}, and the trivial
|
||||
static acks. The client's **observed FUT-hub/economy flow is now fully Rust**; a
|
||||
tail of lower-traffic routes **without a captured wire shape** (item-defs,
|
||||
user-identity, watchList/marketdata, non-active `squad/<n>`, draft, and mode-gated
|
||||
season/tournament/champion/leaderboards/sbs) **still proxy to the Python oracle**.
|
||||
2. **Launcher redesigned to a shareholder-grade egui UI** (your headline ask). Builds
|
||||
clean; screenshots captured.
|
||||
3. **New docs:** `MATCH_LIFECYCLE.md` (you asked), `CORE_CORRECTNESS_ISSUES.md`
|
||||
(4 known Core bugs + 1 bonus exploit, ready to fix on your go/no-go).
|
||||
4. **Nothing committed** — all work is in the working tree for your review (git state is
|
||||
delicate: preserved-dirty Core submodule + a concurrent `funman300` actor + detached
|
||||
launcher branch; I didn't want to entangle that unsupervised).
|
||||
|
||||
## What's live in production now (`10.10.0.120:8099`)
|
||||
|
||||
| Thing | State |
|
||||
|---|---|
|
||||
| prod-host binary | post-P1 `fda40d12` **+ my migration rebuild** (release, in `target/release/openfut-utas-host`) |
|
||||
| prod-host pid | 3207781 (hub-managed, restart=no; retained spec points at the rebuilt binary) |
|
||||
| Catalog | `9f6addaa` (post-P1) |
|
||||
| Core content (cards) | `136d8d68` (post-P1, +18 content-gap defs) → Core loads **1710** defs |
|
||||
| Core owned | **1982** (1962 players + 17 consumables + 3 staff) |
|
||||
| Coins | **29,876,776** (baseline — reset from the P1 test value when the content-gap DB was swapped in; you said data isn't precious) |
|
||||
| prod-core | **fixed build** from canonical submodule (`fbb54ea` + 4 correctness fixes), DB migrated ver 18 → 19; binary now `/home/alex/OpenFUT/target/release/openfut-core` |
|
||||
| UTAS routes (Rust) | economy, club, squad (0/active/list/PUT), account/sync, ut/auth, userMassInfo, **user**, clientdata, hub, settings, accountinfo, leaderboards/options, match/reset, phishing, club/stats/{year,consumables,staff,country,league,team}, watchList, static acks (store/keepalive/captcha/tfa/livemessage/activeMessage) |
|
||||
| Still Python (:8199) | item-defs (item/resource, defid), club-identity (clubUser, user/list, user/club), `squad/<n>` (n≠0), draft, marketdata, mode-gated (season/tournament/champion/leaderboards/sbs → `{}` while off), and match CREATE/READY/PLAY. See `docs/PRODUCTION_AUTHORITY_MATRIX.md`. |
|
||||
|
||||
Smoke-verified live in prod (in the prod netns): all migrated routes return
|
||||
`owner=RUST`, coins consistent, clientdata round-trips, club/stats context modes emit
|
||||
distinct nation/league/team buckets. Scripts:
|
||||
`/home/alex/openfut-promotion/economy-2026-08-17-p2/{p2_precheck,smoke_migrated,smoke_clubstats}.py`.
|
||||
|
||||
## Changes made (all verified: builds clean, tests green)
|
||||
|
||||
### 1. Production promotion (deployed)
|
||||
- Host binary `e5be8730` (P1) → `fda40d12` (post-P1) + catalog `9f6addaa`.
|
||||
- Content-gap DB swapped in (owned 1962 → 1982). Backups in
|
||||
`/home/alex/openfut-promotion/economy-2026-08-17-p2/backup/` + `ROLLBACK.txt`.
|
||||
|
||||
### 2. Route migration to Rust (deployed, rebuilt binary)
|
||||
- `POST /ut/auth` — Rust mints the SID (`OPENFUT-SID-{:016X}`), opens the Rust session,
|
||||
adopts persona from body. No Python. (`+ /ut/delete/auth`.)
|
||||
- `POST /openfut/account/sync` — full Rust envelope; coins/unopenedPacks from Core.
|
||||
- `GET /userMassInfo` — **full** Rust envelope (was a Python-proxy+overlay hybrid).
|
||||
- `GET/PUT /clientdata/<key>` — new host `ClientDataStore` (JSON-persisted).
|
||||
- `GET /club/stats/{country,league,team}` — made `club_stats_body` context-aware
|
||||
(nation/league/team buckets); classify now routes all `club/stats/*` to Rust.
|
||||
- `GET /squad/0` — routed to the Rust active-squad projection (verified structurally
|
||||
identical to Python `squad/0`: same 15 keys, players=23).
|
||||
- `GET /watchList` (+ no-op add/remove) — empty list + authoritative Core credits.
|
||||
- Static acks (`store`, `match/keepalive`, `captcha`, `tfa`, `livemessage`,
|
||||
`activeMessage`) — Rust constants (StaticAck route), byte-identical to the oracle.
|
||||
- Captured the remaining routes' Python wire shapes as reference fixtures for later
|
||||
migration: `docs/evidence/route-shapes-2026-08-17/` (user, defs, marketdata,
|
||||
clubUser, watchList, squad/0, season/tournament/champion/sbs, draft).
|
||||
- Files: `openfut-utas-host/src/{lib.rs,clientdata_store.rs(new),config.rs}`,
|
||||
`openfut-adapter-fifa17/src/fut/{non_economy.rs,club_stats.rs}`,
|
||||
`openfut-utas-host/tests/economy_integration.rs`.
|
||||
- Tests: `openfut-utas-host` + `openfut-adapter-fifa17` full suites **GREEN**
|
||||
(188 adapter + 76 host lib + all integration incl the 116s economy integration).
|
||||
|
||||
### 3. Launcher redesign + polish + live account panel (built, NOT deployed — client tool)
|
||||
- **Redesign**: new `openfut-launcher/src/theme.rs` design system (palette, embedded
|
||||
fonts, egui Visuals/Style, card/pill helpers). Branded hero header (OF monogram),
|
||||
left nav rail, card-based dashboard with status pills, prominent accent Launch CTA,
|
||||
console-style Logs. All existing launch/health/preflight/service/config logic preserved.
|
||||
- **Polish**: OpenFUT window/taskbar icon (OF monogram `IconData`), Config tab rebuilt
|
||||
into themed cards, consistency sweep.
|
||||
- **Live "Your Club" panel** (new feature): a background `AccountMonitor` (mirrors
|
||||
`HealthMonitor`, 5s poll, non-blocking) fetches the account summary and the Dashboard
|
||||
shows a "Your Club" card — club name/abbr, Manager, **COINS hero number**, Level + XP
|
||||
bar, unopened packs, account funds — with clean loading/offline/error states.
|
||||
- Builds clean (0 warnings). Screenshots preserved (for your shareholder demo) in
|
||||
`/home/alex/openfut-post-p1/launcher-screenshots-2026-08-17/` — `launcher_account.png`
|
||||
(the populated "Your Club" card: COINS 29,876,776, Level 12, packs 3) is the headline;
|
||||
plus dashboard/setup/logs/config + the offline state. All reviewed — product-quality.
|
||||
- Files: `openfut-launcher/src/{theme.rs(new),account_monitor.rs(new),app.rs,main.rs,
|
||||
account_sync.rs,config.rs}` + `assets/` (fonts + icon). All additive; behavior preserved.
|
||||
|
||||
### 4. Core correctness fixes (deployed 2026-08-17)
|
||||
|
||||
All four `CORE_CORRECTNESS_ISSUES.md` classes fixed in the canonical `openfut-core`
|
||||
submodule and deployed to prod-core (see that doc's "Resolution" section):
|
||||
- **Issue 3 (HIGH, exploit):** SBC duplicate-card free-reward — `submit_sbc` now dedups
|
||||
ids + bounds the list (`MAX_SBC_CARDS`=30) → `BadRequest`. Regression test added.
|
||||
- **Issue 2 (HIGH):** non-atomic economy mutations — closed the concurrency-exploit
|
||||
races with single-statement compare-and-swap (`spend_coins` conditional debit,
|
||||
`open_pack`/`buy_listing`/`sell_card`/`checkin` claim-then-act). Multi-statement
|
||||
partial-failure edges (`buy_pack`, concurrent SBC, match/season chains) left as
|
||||
documented residual — need the transaction refactor, negligible for single-player.
|
||||
- **Issue 4 (LOW):** `season.rs` `.expect()` panics → graceful `AppError`; checkin index
|
||||
`% 7` → `.rem_euclid(7)`.
|
||||
- **Issue 1 (LOW):** `sbc_submissions.club_id` — migration `0019` (add + backfill) +
|
||||
`submit_sbc` binds it; milestone query now correct.
|
||||
- Verified: Core suite **179 green** + clippy clean; migration dry-run on a prod-DB copy;
|
||||
post-deploy prod migration ver 19, owned 1982, coins 29,876,776, all Core + host
|
||||
endpoints 200. Rollback: `backup/prod-core.preCoreFix.db` (ver 18) + old binary path —
|
||||
see `backup/ROLLBACK_CORE_FIX.txt`.
|
||||
|
||||
## New / updated docs
|
||||
|
||||
- `docs/MATCH_LIFECYCLE.md` (NEW) — consolidated FUT match loop design (CREATE→READY→
|
||||
PLAY→END), contracts, reward policy, ownership split, blockers. (You asked for this.)
|
||||
- `docs/CORE_CORRECTNESS_ISSUES.md` (NEW) — 4 known Core bugs + 1 bonus SBC
|
||||
duplicate-card exploit, each with file:line + concrete fix + severity. **Needs your
|
||||
go/no-go** (fixing bumps Core off the frozen P1 reference).
|
||||
- `docs/PRODUCTION_AUTHORITY_MATRIX.md` (UPDATED) — reflects the completed migration.
|
||||
- Vault `06 Agent Memory/Current Priorities.md` (UPDATED).
|
||||
|
||||
## Uncommitted work — needs your review
|
||||
|
||||
**I committed nothing** (git state is delicate: openfut-core is intentionally
|
||||
preserved-dirty; a concurrent `funman300` actor; launcher on detached HEAD `d1a71bd`).
|
||||
Review + commit these when you're ready:
|
||||
|
||||
- Superproject (mine): `openfut-utas-host/src/{lib.rs,config.rs}`,
|
||||
`openfut-utas-host/src/clientdata_store.rs`,
|
||||
`openfut-utas-host/tests/{economy_integration.rs,host_test.rs}`,
|
||||
`openfut-adapter-fifa17/src/fut/{club_stats.rs,non_economy.rs}`,
|
||||
`docs/{PRODUCTION_AUTHORITY_MATRIX.md,MATCH_LIFECYCLE.md,CORE_CORRECTNESS_ISSUES.md,OVERNIGHT_HANDOFF_2026-08-17.md,PYTHON_RETIREMENT_PLAN.md}`,
|
||||
and the reference fixtures `docs/evidence/route-shapes-2026-08-17/`.
|
||||
- Launcher submodule (mine): `src/{app.rs,main.rs,theme.rs(new),account_monitor.rs(new),account_sync.rs,config.rs}`, `assets/` (fonts + icon).
|
||||
- **Leave the pre-existing dirt alone** (not mine): `CLAUDE.md`, `README.md`,
|
||||
`.env.example`, `docker-compose.yml`, `AGENTS.md`, `setup.sh`, `openfut-bridge`,
|
||||
`fifa17-recon/docker/...`, `docs/{ARCHITECTURE,ROADMAP,docker,fifa17-emulation}.md`,
|
||||
`docs/research/`, `scripts/utas-filter-diff.py`.
|
||||
- `openfut-core` (mine, this session): `migrations/0019_sbc_submissions_club_id.sql` (new),
|
||||
`src/services/{sbc.rs,club.rs,pack.rs,market.rs,checkin.rs,season.rs}`,
|
||||
`tests/integration_test.rs`. Built + deployed to prod-core; still detached HEAD at
|
||||
`fbb54ea` (edits uncommitted, per your branch strategy).
|
||||
|
||||
## Open decisions for you
|
||||
|
||||
1. ~~**Core correctness bugs**~~ — **DONE (2026-08-17):** all four classes fixed +
|
||||
deployed to prod-core (see "Core correctness fixes" above and the Resolution section
|
||||
of `docs/CORE_CORRECTNESS_ISSUES.md`). prod-core is now off the frozen P1 point,
|
||||
running `fbb54ea` + fixes at migration ver 19. Residual (documented): the
|
||||
multi-statement transaction refactor for partial-failure atomicity — negligible for
|
||||
single-player; do it if/when concurrency matters.
|
||||
2. **Match handshake legs** (CREATE/READY/PLAY) — the only routes still on Python.
|
||||
Deferred: no match has ever been played in-game, READY `items` contract unknown,
|
||||
`FUT_MODES` off (see `docs/MATCH_LIFECYCLE.md`). Migrating them risks the economy
|
||||
`/match/end` routing for a never-exercised path — I judged it not worth it unmonitored.
|
||||
3. **Aux service container cutover** (blaze/redirector/roster → Rust) — operator-gated
|
||||
container change; unchanged.
|
||||
|
||||
## Rollback (still hot)
|
||||
|
||||
- Python P2 image `openfut-fut-backend:p2-rollback` (b1b929953f) + profile 39bb3e83 +
|
||||
`rollback-to-python-p2.sh`.
|
||||
- prod-host P1 binary + P1 catalog backed up in
|
||||
`/home/alex/openfut-promotion/economy-2026-08-17-p2/backup/` (see `ROLLBACK.txt`).
|
||||
- prod state (pre-content-gap) backed up: `backup/prod-core.preB.db`,
|
||||
`prod-identity.preB.json`, plus P1 `fifa17-production-{catalog,cards}.p1.json`.
|
||||
@@ -1,165 +0,0 @@
|
||||
# Production Authority Matrix (post-P1 -> P2-routes promoted 2026-08-17)
|
||||
|
||||
> **P2-routes promoted to production 2026-08-17.** prod-host swapped P1 `e5be8730` -> post-P1
|
||||
> `fda40d12`; catalog `35a0913b` -> `9f6addaa` (resolves all owned assets, dropped_no_asset=0).
|
||||
> Every previously-Python non-economy route now RUST in prod (OBSERVED prod log). Rollback hot:
|
||||
> restore P1 binary + backup catalog (`economy-2026-08-17-p2/backup/`).
|
||||
|
||||
Definitive inventory of every production-reachable FIFA17 route/service and its
|
||||
current owner. Derived from the live `prod-host` dispatch log (owner= labels,
|
||||
real Client A session 2026-08-14), `openfut-utas-host/ROUTE_AUTHORITY.md`, and the
|
||||
prod container config (`OPENFUT_SERVERS="blaze roster pow"`).
|
||||
|
||||
Evidence labels: OBSERVED (live log/db), PROVEN (test), INFERRED, HYPOTHESIS.
|
||||
|
||||
Legend: owner R = Rust/Core, P = Python oracle (:8199 proxied via PYTHON_FALLBACK).
|
||||
|
||||
## UTAS HTTP (front door: openfut-utas-host :8099)
|
||||
|
||||
### ECONOMY — already Rust (Python economy hits = 0, OBSERVED)
|
||||
| Method/path (tail) | Prod owner | Writes state | Rust handler | Py proxy |
|
||||
|---|---|---|---|---|
|
||||
| GET /user/credits | R | no | handle_credits | NO |
|
||||
| GET /store/purchasegroup[/all] | R | no | handle_purchasegroup | NO |
|
||||
| PUT /store/transaction[/<id>] | R | coins,inv,pile | handle_store_buy | NO |
|
||||
| POST /purchased[/items] | R | coins,inv,ent,pile | handle_pack_open | NO |
|
||||
| GET /purchased[/items] | R | no | shape_purchased_reveal | NO |
|
||||
| DELETE /item/<id> | R | coins,inv | handle_quick_sell_path | NO |
|
||||
| POST /ut/delete/../item | R | coins,inv | handle_quick_sell_body | NO |
|
||||
| PUT /item | R | inv,pile | handle_move_items | NO |
|
||||
| POST /ut/delete/../match | R | coins | handle_match_end | NO |
|
||||
| POST /auctionhouse,/transfermarket | R | listings | handle_market_list | NO |
|
||||
| GET /tradePile | R | no | handle_market_query | NO |
|
||||
| GET /tradePile/counts | R | no | handle_market_counts | NO |
|
||||
| /trade/<id> (POST/PUT/GET) | R | coins,inv,listings | handle_market_buy | NO |
|
||||
| DELETE /ut/delete/../trade/<id> | R | listings | handle_market_cancel | NO |
|
||||
|
||||
**Transfer-market semantics (live-verified 2026-08-17).** Three things here are
|
||||
load-bearing and were each a live defect:
|
||||
1. `/tradePile` and `/tradePile/counts` are **different deserializers** and MUST NOT
|
||||
share a handler. `/counts` is FutGetAuctionCount: five scalar ints
|
||||
(`count`, `maxAuctionsAllowed`, `offered`, `selling`, `sold`) and nothing else.
|
||||
Served the `auctionInfo` body it skips every field, leaving the counts at 0 — the
|
||||
hub tile shows a listing while the Transfer List screen shows no active sale.
|
||||
2. An auction record's `itemData` MUST be the **full card object** (the same shape
|
||||
`/club` emits), not a stub. A listing therefore persists a shaped-card SNAPSHOT
|
||||
(`listings.item_json`) at list time. The seller's own pile stamps
|
||||
`itemState: listFS`; market search uses `forSale`.
|
||||
3. `POST /auctionhouse` resolves the listed card **server-side** from the wire item
|
||||
id (`wire → Core instance → card_id + resourceId`); the client's FutISStart body
|
||||
carries only the id. This also enforces that you can only list what you own.
|
||||
|
||||
### NON-ECONOMY — Rust-owned (route migration 2026-08-17, OBSERVED prod log)
|
||||
| Route | Owner | Notes |
|
||||
|---|---|---|
|
||||
| POST /ut/auth (+ /ut/delete/auth) | R | Rust mints the SID (OPENFUT-SID-{:016X}); opens Rust session; adopts persona from body. No Python. |
|
||||
| POST /openfut/account/sync | R | full Rust envelope; coins/unopenedPacks from Core; clubName OpenFUT / clubAbbr OFC constants |
|
||||
| GET /userMassInfo | R | FULL Rust envelope (userInfo+squad+settings+pileSizeClientData); no Python. coins from Core, squad == /squad/active |
|
||||
| GET/PUT /clientdata/<key> | R | host ClientDataStore (JSON-persisted); PUT acks {}, GET returns blob or {} |
|
||||
| capability (/openfut/fifa17/capability) | R | -> Bound (CleanV1) |
|
||||
| GET /club, /club/* readers | R | Core-backed collection (dropped_no_asset=0). Cards with an **ACTIVE listing are excluded** — a listed card has left the club. Keyed on the listing, NOT on the `trade` pile: the pile can hold cards with no listing (bare move, or cancelled/sold) and `/tradePile` renders only ACTIVE listings, so hiding the pile would make those invisible in BOTH views. Keying on the listing is self-healing — cancel/sale restores club visibility with no extra transition. Pagination then runs over the club-visible set (Core cannot filter host-owned listing state, so letting it paginate would yield short pages); with nothing hidden the fast Core-paginated path is unchanged. |
|
||||
| GET /squad/0, /squad/active, /squad/list; PUT /squad/<n> | R | Core squad projection + tx; GET /squad/0 == active squad (verified structurally identical) |
|
||||
| GET /user/accountinfo | R | {} |
|
||||
| GET /user | R | `{"userInfo": …}` — same userInfo builder as userMassInfo (shared); squad rating is Core-authoritative (DIFFERENT-BY-DESIGN vs Python's stale value) |
|
||||
| GET /settings | R | {"configs":[]} |
|
||||
| GET /leaderboards/options | R | {} |
|
||||
| PUT /match/reset | R | {} |
|
||||
| GET /phishing/trusteddevice | R | security-question stateless ack |
|
||||
| GET /hub | R | Core-derived counts; `clubPlayers` excludes actively-listed cards, `auctionCount`/`tradePile` from the durable market store |
|
||||
| GET /club/stats/{year,consumables,staff,country,league,team} | R | Core aggregation; context buckets keyed nation/league/team (owned=1982); staff={} |
|
||||
| GET /store, /match/keepalive, /captcha, /tfa, /livemessage, /activeMessage | R | unconditional constant acks (byte-identical to the oracle; StaticAck route) |
|
||||
| GET /watchList (+ PUT/POST/DELETE) | R | empty watch list + authoritative Core credits; add/remove is a no-op ack (oracle persists none) |
|
||||
| GET /season, /tournament, /champion, /clubUser, /user/list | R | FUT modes + club-identity off → `{}` (FeatureOffEmpty; byte-identical to the flag-off oracle). Migrated + deployed 2026-08-17 |
|
||||
| POST /ut/.../match/end (DestroyMatch) | R | economy reward (coins credited via Core grant_reward) |
|
||||
| GET /item/resource, /defid | R | `{itemData:[item_def…]}` — asset=rid&0xffffff; hardcoded Ronaldo (20801) + placeholder ("Player",75,CM,attrs 70), mirroring the oracle's `item_def`. Client renders from its LOCAL DB, so the placeholder is exact parity. Migrated + deployed 2026-08-17 |
|
||||
| GET /marketdata, /marketdata/pricelimits | R | suggested pricing, constant band 150..15000. `/pricelimits` = bare ARRAY (one per defId); plain `/marketdata` = OBJECT — container type is load-bearing (object-where-array froze a live client). Migrated + deployed 2026-08-17 |
|
||||
|
||||
### NON-ECONOMY — still Python (PYTHON_FALLBACK)
|
||||
| Method/path | Owner | Reason |
|
||||
|---|---|---|
|
||||
| POST /user/club (rename) | P | mutating club rename; Core has `clubs` but rename needs a Core write (deferred). Reads `clubUser`/`user/list` are now Rust. |
|
||||
| GET /squad/<n> (n≠0, non-active) | P | no multi-squad Core model (Rust owns squad/0, squad/active, /squad/list, PUT) |
|
||||
| /squad/mode/draft/* | P | FUT Draft mode |
|
||||
| GET /leaderboards, /sbs/* | P | mode-gated (FUT_MODES/_SBC off → `{}`/content; `/sbs/sets` ships content); real behavior needs the mode logic ported. `season`/`tournament`/`champion` are now Rust. |
|
||||
| POST /ut/.../match (CREATE), /match/ready (READY), /match (PLAY) | P | match handshake legs; no match ever played in-game (see docs/MATCH_LIFECYCLE.md) |
|
||||
|
||||
## AUXILIARY SERVICES (prod container OPENFUT_SERVERS="blaze roster pow")
|
||||
All aux services are **Python in production today** (container `entrypoint.sh` runs
|
||||
`blaze_responder_v3b.py`/`roster_server.py`/`pow_server.py`). Rust equivalents exist
|
||||
outside Docker; deploying them is operator-gated (production deployment forbidden here).
|
||||
|
||||
| Service | Rust crate | Completeness | Prod owner | Reachable | Blocker to candidate |
|
||||
|---|---|---|---|---|---|
|
||||
| Blaze (:42130) | openfut-blaze-host + openfut-protocol-blaze + adapter::blaze | COMPLETE, gate-proven to real FUT (Gate 10: 3 logins, 10 pack opens, 448/448 py suite) | Python | yes | ERRC error-reply placement unresolved; wire into candidate bring-up |
|
||||
| Redirector (:42127 TLS) | openfut-redirector-host + openfut-tls (OpenSSL vendored) | COMPLETE, 1 live handshake 2026-08-11 (TLSv1.2/AES256-GCM-SHA384) | Python | yes | never full-path gated; cert consistency |
|
||||
| Roster (:8081) | openfut-roster-host | COMPLETE, oracle-parity + lifecycle tests, unit-only | Python | yes | never live-gated |
|
||||
| POW (:8094 + :8080) | NONE (only pow_server.py) | no Rust host; 58 templates, bodies placeholder | Python | yes | LARGEST: no crate + bodies un-reversed |
|
||||
| Nucleus (:42131) | none (advertised string only) | n/a | Python stub (advertised, unused) | NO (0 live hits) | DEAD in current flow — keep advertising URL, no migration |
|
||||
|
||||
RETIRED/out-of-scope: fifa-blaze (FIFA23 capture stub), openfut-bridge (FIFA23). Not in FIFA17 flow.
|
||||
|
||||
## NON-ECONOMY UTAS TARGET OWNER (confirmed via scouts)
|
||||
Core already exposes GET/PUT /settings, GET /club|/collection|/statistics|/profile, /squad/*.
|
||||
New Rust arm = Route variant + classify() arm (lib.rs:118-140) + owner-labelled handler.
|
||||
|
||||
| Route | Port complexity | Target | Notes |
|
||||
|---|---|---|---|
|
||||
| user/accountinfo | trivial-static ({}) | R | host constant |
|
||||
| settings | trivial-static ({"configs":[]}) | R | host constant / Core /settings |
|
||||
| leaderboards/options | trivial-static | R | host constant |
|
||||
| match/reset | trivial-static ({}) | R | host constant ack |
|
||||
| phishing/trusteddevice | small (validate hex + constant) | R | security-question: stateless ack, always verified/trusted |
|
||||
| clientdata/userHubData | small stateful | R | Core PUT /settings upsert keyed userHubData |
|
||||
| hub | medium (derived counts) | R | Core collection + market counts |
|
||||
| club/stats/year | medium (rating-tier aggregation) | R | Core /collection + /statistics |
|
||||
| club/stats/consumables | medium | R | BLOCKED on 17-consumable import |
|
||||
| club/stats/staff | trivial ({}) or derived | R | BLOCKED on 3-staff import |
|
||||
| account/sync | small-medium (persona select + save) | R | hardest: no direct Core route; host session/persona logic |
|
||||
| ut/auth envelope | small (SID mint) | R (currently OBSERVE) | Python still mints envelope; boundary decision |
|
||||
|
||||
## MIGRATION PRIORITY ORDER (Phase 12)
|
||||
1. Content gap consumable+staff emit (unblocks club/stats/{consumables,staff}) — import crate.
|
||||
2. Trivial-static host routes: accountinfo, settings, leaderboards/options, match/reset, phishing/trusteddevice.
|
||||
3. clientdata/userHubData (Core settings upsert).
|
||||
4. hub + club/stats/year (Core-derived aggregation).
|
||||
5. userMassInfo full ownership (envelope scaffold; econ+squad already Rust).
|
||||
6. account/sync + ut/auth envelope (account/session boundary).
|
||||
7. Aux candidate wiring (blaze/roster/redirector already built) + POW (blocked) + Nucleus (dead).
|
||||
|
||||
## ECONOMY EXPECTATION
|
||||
Python economy hits = 0 (OBSERVED live + PROVEN by NEVER-BOTH/no-fallback tests).
|
||||
Any nonzero Python economy hit = P1 regression, priority zero.
|
||||
|
||||
## STATUS
|
||||
Baseline + economy + Rust-owned non-economy rows: OBSERVED/PROVEN.
|
||||
Aux + non-economy target owners: confirmed via HostSourceMap/PythonContractMap/AuxServiceMap/ContentGapMap scouts.
|
||||
|
||||
## CANDIDATE MIGRATION STATUS (post-P1 progress, off-production)
|
||||
DONE (Rust-owned in candidate source; committed on top of 5020137; workspace tests green):
|
||||
- Content gap: consumable+staff emit (20 instances / 18 defs; verified real-profile re-import 1962+20).
|
||||
- Non-economy routes migrated to Rust: user/accountinfo, settings, leaderboards/options,
|
||||
match/reset, phishing/{trusteddevice,question,validate}, club/stats/staff, hub,
|
||||
club/stats/{year,consumables}.
|
||||
(hub clubPlayers = owned player count from Core; may be < Python profile count by the deferred
|
||||
Legend instances = DIFFERENT-BY-DESIGN.)
|
||||
- Reachability reporter (scripts/openfut-reachability.py) gates Python-hit invariants.
|
||||
|
||||
RESIDUAL PYTHON (still proxied; each has a concrete blocker, not ordinary difficulty):
|
||||
- club/stats/{country,league,team} — RESIDUAL (nation/league/team context sub-screens). The global
|
||||
MY-CLUB stat set (club/stats/{year,consumables,staff}) is now Rust (adapter club_stats.rs faithful
|
||||
port of fut_club_stats.py VOCAB + counts, Core-accurate over real imported content, staging-verified
|
||||
RUST route=club-stats owned=1982 players=1962). The per-nation-bucket context sub-screens still proxy
|
||||
Python (low value, unrecognized atoms inert); migrate with a live context capture if ever needed.
|
||||
- account/sync — launcher-facing (POST /openfut/account/sync, pre-auth); envelope has fields not in
|
||||
Core (clubAbbr, established, profilePath, accountFunds cap); changing it risks the launcher. Needs
|
||||
a Core account endpoint or host account logic + launcher-contract verification.
|
||||
- userMassInfo — SAFE hybrid today (Rust overlays economy+squad on Python envelope). Full ownership
|
||||
needs a real full-envelope capture first (missing one scaffold field breaks a hot route); deferred.
|
||||
- ut/auth — ORDERING-BLOCKED: still-proxied Python routes rely on Python's session table, so auth
|
||||
must stay proxied (Python mints SID, Rust OBSERVEs) until every session-needing route is Rust. Migrate LAST.
|
||||
- clientdata/userHubData — BLOCKED: Core /settings supports only 2 fixed keys (difficulty,
|
||||
preferred_formation); storing an arbitrary blob needs a frozen-Core change or a new host store.
|
||||
Low-value UI-pref blob; kept Python per Phase 23 (no Python-by-ideology).
|
||||
|
||||
AUX (Rust built, deploy = operator-gated container cutover, NOT this task): blaze/roster/redirector.
|
||||
POW = blocked (no crate, bodies un-reversed). Nucleus = dead (0 live hits).
|
||||
@@ -1,192 +0,0 @@
|
||||
# OpenFUT — Project State
|
||||
|
||||
> **Canonical source:** `../OpenFUT-Vault/06 Agent Memory/Project State.md`
|
||||
> This file is a mirror. If the two disagree, the vault wins. Update the vault first.
|
||||
|
||||
Factual snapshot. Prefer this over the stale root `README.md`/`CLAUDE.md` status tables (FIFA 23).
|
||||
Last compiled from repository evidence during context initialization.
|
||||
|
||||
## Working
|
||||
|
||||
- **FIFA 17 offline FUT stack, end-to-end.** Proven 2026-08-01: auth → Blaze login → device-trust →
|
||||
the FUT hub. Brought up by `fifa17-recon/tools/openfut-fut.sh start`. Evidence: `FUT-RUNBOOK.md`,
|
||||
`fifa17-recon/README.md`, the five responder scripts, gate-ladder troubleshooting table.
|
||||
- **ProtoSSL cert-pin defeat** — two live `/proc/PID/mem` patches (`autopatch.py`), VAs stable
|
||||
across launches. gdb-verified which gate was the wall.
|
||||
- **LSX / Origin layer** — crypto handshake reversed byte-exact and confirmed against the repack's
|
||||
own emu disassembly (`docs/REPACK_INTEL.md`); Origin login gates cleared.
|
||||
- **Blaze redirector + Fire2/Heat2** — both hops defeated; preAuth/login/personas answered.
|
||||
- **UTAS/RS4 FUT API** — `ut/auth` + boot calls + device-trust reach the hub with hand-authored JSON.
|
||||
- **Persistent FIFA 17 account selection** — the launcher synchronizes one configured EA persona to
|
||||
the Python backend before starting LSX/FIFA. LSX, Blaze, POW/EASFC, and UTAS then share that
|
||||
identity, while FUT coins, inventory, squads, progression, and unopened packs persist in an
|
||||
isolated save beneath `fifa17-recon/docker/state/accounts/<persona-id>/`. The POW level/XP/funds
|
||||
shown in FIFA's general account bar are account-scoped but remain distinct from FUT club coins.
|
||||
A reversible server test on 2026-08-09 verified profile switching, POW values, a 400-coin pack
|
||||
debit, five awarded items, and restoration of the original profile.
|
||||
- **Account-scoped FUT security compatibility** — launcher account synchronization initializes a
|
||||
persisted `securityQuestion` verification record in that persona's FIFA 17 profile. The UTAS
|
||||
PHISHING handler returns the complete CardsDLL trusted-console response (`changed`, `exists`,
|
||||
`locked`, `trusted`), accepts only well-formed legacy setup/validate requests under `X-UT-SID`,
|
||||
and never stores or logs the client-transformed answer. This is server-side emulation; the hook
|
||||
and launcher do not contain an answer or add UI automation. Automated contract coverage is in
|
||||
`fifa17-recon/docker/ctx/tools/test_security_question.py`; live first/repeat-launch acceptance is
|
||||
partially complete: the first launch entered FUT without a security dialog on 2026-08-09; a
|
||||
second fresh-process FUT entry is still required to close persistence acceptance.
|
||||
- **Safe responder diagnostics** — ordinary LSX logs redact challenge/session/auth-code attributes;
|
||||
ordinary Blaze logs redact auth/session keys and no longer emit raw Fire2 hex, decoded TDF, or
|
||||
config values. Forensic Blaze capture remains available only with the explicit
|
||||
`OPENFUT_BLAZE_DUMP_FRAMES=1` opt-in. LSX and Blaze self-tests cover the new defaults.
|
||||
- **OpenFUT Core** — Rust FUT economy backend, feature-complete for its scope and tested: profiles,
|
||||
clubs, coins, packs, cards, squads, chemistry styles, SBCs, objectives, matches, market (NPC),
|
||||
draft, FUT Champs, seasons, statistics, achievements, events, daily check-in, division
|
||||
leaderboard, market trade history. 13 migrations. Integration suite (`tests/integration_test.rs`,
|
||||
96 test fns) runs against in-memory SQLite; CI (fmt/clippy/build/test) green on `openfut-core`.
|
||||
|
||||
## Partially implemented
|
||||
|
||||
- **FIFA 17 FUT hub depth** — reaching the hub is proven, but how much of FUT is fully navigable
|
||||
beyond it (playing matches, pack opening, SBC submission through the *game* UI vs. spinner/error
|
||||
states) is not documented as complete. The runbook's gate ladder lists failure modes still
|
||||
guarded against. Treat "past the hub" as unverified.
|
||||
- **Pack opening through the game UI** — proven live on 2026-08-09 with the recovered CAGE test
|
||||
profile: purchase, reveal, item assignment/quick-sell, wallet refresh, and return from the reveal
|
||||
all completed. The Python transaction path also passes its 446-check contract suite. FIFA's
|
||||
hardcoded post-reveal `mypacks` return is supported by a short-lived active grace record for every
|
||||
opened pack; it is excluded from unopened-pack counts and retired at the next hub request.
|
||||
- **FIFA 17 FUT match lifecycle** — CardsDLL static analysis and isolated responder tests now cover
|
||||
CREATE→READY→PLAY→END. Bare `/match` requests carrying body `matchId` are classified as PLAY
|
||||
instead of accidentally allocating another match; READY returns the verified scalar `matchId`
|
||||
and `opponentPersonaId` fields; END persists W/D/L, matches played, and coin rewards per account.
|
||||
The implementation is deployed and `test_match_lifecycle.py` passes, but no football match has
|
||||
started or completed in FIFA yet. The READY opponent `items` contract and client mode-entry gate
|
||||
remain unresolved; `FUT_MODES` therefore stays off by default.
|
||||
- **Core ↔ emulation integration** — the two halves exist and wiring has **started**. First
|
||||
slice (2026-08-11): the My Squad owned-player search. `openfut-core` gained a semantic,
|
||||
game-independent owned-inventory query (`services::inventory::{OwnedItemQuery, apply_query}`
|
||||
+ a `Quality` tier) that filters (AND) → orders deterministically → paginates, wired into
|
||||
`GET /collection`; `openfut-adapter-fifa17::fut::owned_query` parses the FIFA17 `/club` wire
|
||||
query and resolves numeric league/nation/team ids → semantic names (unknown id = hard error,
|
||||
no raw-id passthrough). Intentional fix, not parity: Python applies only `league`+`team` and
|
||||
ignores `level`/`rare`/`position`/`nation`/`start`/`count` (the request-amplification bug);
|
||||
Core applies all proven filters and paginates. `rare=SP` semantics UNKNOWN, unimplemented.
|
||||
Slice 2 (2026-08-11): `openfut-utas-host` — the first live UTAS host. Serves `GET …/club`
|
||||
from Core through the adapter and reverse-proxies every other UTAS route verbatim to the
|
||||
Python oracle (`utas_server.py`); plaintext HTTP/1.1 keep-alive, classify-before-execute,
|
||||
no python-fallback after a Core error. `CoreAccess` is a host-owned boundary (the adapter
|
||||
stays transport-agnostic). 11 host + 22 adapter tests; 10/10 mutations killed; fmt/clippy
|
||||
clean. Slice 3 (2026-08-11, `3ef3bc3`): the real `Fifa17IdentityResolver` — catalog
|
||||
(card id → real asset id) + persistent `openfut-identity` store (owned instance →
|
||||
stable/reversible wire int) + wire-id policy, replacing all placeholders (one
|
||||
production path). Wire-id namespace is globally monotonic within `(fifa17, owned-item)`,
|
||||
not per-account (Core owned ids are UUIDs → unambiguous reverse). Slice 4 (2026-08-11,
|
||||
core `36abd4b`): a curated 32-card real FIFA17 dev content pack
|
||||
(`data/games/fifa17/dev/cards.json`, ids `fifa17_<asset>`), loaded only via opt-in
|
||||
`Config.dev_content_games`; `seed-dev` grants a `game_id=fifa17` profile+club real
|
||||
`OwnedCard`s (no FIFA wire ids — the resolver mints those at request time), idempotent,
|
||||
default content untouched. Slice 5 (2026-08-11, `5276dd2`): the host sends
|
||||
`X-OpenFUT-Game: fifa17` so `/club` resolves the all-mapped fifa17 profile —
|
||||
**composition proven live** over HTTP (real Core+host, no FIFA client): FIFA wire query
|
||||
→ real `resourceId`s (catalog) + stable/reversible wire `id`s (store); 33 renderable,
|
||||
gold=22, Premier League=18, pagination page1=11/page2=7/overlap=0 (clean paging, no
|
||||
drops). **The only remaining gate is the live retail FIFA A/B** (no FIFA client in the
|
||||
build env; runbook `openfut-utas-host/README.md`). See the vault UTAS Endpoint Map +
|
||||
Known Issues (incl. "identity resolution is NOT authorization" for later mutations).
|
||||
**Slice 6 (2026-08-11): `/club` RUNTIME VALIDATED on retail FIFA 17** — operator-assisted
|
||||
live A/B (`.105` client → `.120` backend via a source-scoped NAT redirect into a staged
|
||||
`36abd4b` Core + `openfut-utas-host`). Every checkpoint passed on the real client:
|
||||
transport, per-route Python fallback, Python-negative `/club`, Core `X-OpenFUT-Game`
|
||||
scoping, real card + persistent owned-item identity (incl. the two-copy fixture),
|
||||
no-filter/Gold/position/nation/league/team/combined-AND filtering, retail pagination
|
||||
with no amplification, card selection, identity stability across relaunch, Python
|
||||
rollback, and Rust re-enable (identity store byte-identical across the cycle, 0
|
||||
reallocation). Live findings: the retail client steps `start += 10` with `count=11`
|
||||
(sometimes bulk `count=100`) — Core honours `offset` and terminates; the My Club UI
|
||||
nests team under league; the "~1900" club counter is Python `userMassInfo`, not `/club`.
|
||||
Remaining is operational only (promote the staged stack to a durable deployment).
|
||||
- **Slice 7 (2026-08-12): FUT squad authority (read + write) RUNTIME VALIDATED on retail FIFA 17.**
|
||||
Staged operator-assisted A/B (`.105` retail client → `.120`, source-scoped utas switch
|
||||
`:8099→:8199` into `openfut-utas-host` over a staged `615c5fd` Core seeded by `seed-dev` with the
|
||||
dev 33-card inventory). FIFA itself consumed the Rust squad path end-to-end: FUT boot served
|
||||
`RUST_OVERLAY userMassInfo` (squad overlay) and the squad screen rendered the dev XI; a controlled
|
||||
in-game squad edit issued `PUT …/squad/0` → host `squad-replace` → Core → `{"id":0}`; an in-game
|
||||
formation change f442→f433 persisted to Core (canonical fingerprint changed, `position_index`
|
||||
remapped 0–10); a FULL FIFA relaunch cold-fetched and rendered the persisted f433 squad (no client
|
||||
cache). Reversibility proven on the exact client path by log presence, not response data (both
|
||||
backends coincidentally hold the same dev squad — the Python oracle `fifa17_profile.json` was
|
||||
seeded from the same `squad_put_f442.json` capture): disarmed `.105:8099` reached Python (absent
|
||||
from host log), re-armed reached Rust (`route=club limit=Some(9)` present); the persistent identity
|
||||
store survived the cycle with 0 reallocation. Non-migrated routes (`/ut/auth`, `account/sync`,
|
||||
`accountinfo`, `settings`, `hub`, store txn) correctly Python-fallback. NEW startup requirement:
|
||||
the Core server loads dev card defs only for games in env `OPENFUT_DEV_CONTENT_GAMES` (comma-sep);
|
||||
omitting `fifa17` makes `get_collection` silently drop every owned card (empty `/collection`,
|
||||
"no squad") despite a successful seed — MUST become a deployment/preflight assertion. Preceded the
|
||||
same day by a staged two-process parity gate (real Core+host over HTTP, no FIFA) confirming byte-
|
||||
shape parity of `userMassInfo.squad` vs the captured oracle. Next milestone: real-data Core
|
||||
import / profile strategy → production Rust UTAS. Blaze deferred.
|
||||
- **Slice 8 (2026-08-12): REAL-DATA staged retail A/B PASS (import + club + squad).** The real FIFA 17
|
||||
profile was imported into a staged Core via the two-store protocol (`openfut-import-fifa17 --apply`:
|
||||
generic transactional Core import + `openfut-identity` seeding, idempotent), then FIFA itself
|
||||
consumed it end-to-end over the source-scoped utas switch (`.105`→`.120:8199`). Imported population
|
||||
1949 of 1962 player instances (13 Legend/special assets deferred as unnameable — 9 NoName + the 4
|
||||
copies of `169193`), every original Python wire id preserved (set-equal, 0 minted/dropped), each
|
||||
owned instance an opaque Core UUID. On the retail client: real club rendered (1949, not the 33-card
|
||||
dev XI); `/club` pagination clean (paged==full, no loop/overlap/drop); the `Special` quality filter
|
||||
returned only specials (1665, 0 base leaked) and paginated the filtered set; a squad edit persisted
|
||||
to Core (canonical + opaque extension atomically, fingerprint recomputed) and survived a full cold
|
||||
relaunch; rollback→Python→Rust re-enable proven on the exact client path (disarm reached Python,
|
||||
re-arm returned the imported club + edited squad; identity store 0 reallocation). Three real-data
|
||||
fidelity gaps the base-only dev fixture had hidden were found on the live client and fixed:
|
||||
(1) `e187cd4` versioned `resourceId` — `shape_item` emitted the base assetId as `resourceId`,
|
||||
collapsing every special onto its base card art; `Fifa17Identity` now carries the versioned
|
||||
`resource_id`. (2) `626c972` observed `rareflag` — `shape_item` hardcoded `rareflag=1`, so all
|
||||
specials rendered as basic rare; `rareflag` now flows through the FIFA catalog and onto the wire
|
||||
(wire distribution == source exactly). (3) `6f16a23` `rare=SP` Special filter — previously a no-op
|
||||
("semantics UNKNOWN"), now grounded as `rareflag > 1` and applied host-side (Core has no rareflag).
|
||||
Also `44fcf24`: nation/league/team proven to be INSTANCE metadata (not definition identity — the 4
|
||||
copies of `169193` are identical but for club), so the definition-consistency gate now compares
|
||||
identity only (no `--defer-conflict` needed; no majority-vote). PRODUCTION NOT READY: the 13 deferred
|
||||
Legends are unnameable from the `.120` client dump (absent/placeholder in `players.json`; DLC/Legends
|
||||
name tables empty) — honest names require the FIFA 17 Legends locale data from the `.105` client.
|
||||
Next: resolve the 13 names → re-import to 1962/0 → full-fidelity gate → gitlink reconcile →
|
||||
production Rust UTAS.
|
||||
|
||||
## Stubbed / planned
|
||||
|
||||
- **`fifa-blaze`** (Rust) — Milestone 1 capture stub only. Two TLS listeners that log packets; no
|
||||
FIFA 23 component/command handlers. Its own README says IDs are unknown. Superseded in practice by
|
||||
the Python FIFA 17 responders, kept as the intended FIFA 23 implementation surface.
|
||||
- **`openfut-launcher` legacy controls** — core/bridge and FIFA 23 setup controls belong to a
|
||||
superseded plan. The launcher now also owns the live FIFA 17 client flow: server/hook config,
|
||||
account synchronization, local LSX, privileged autopatch, and game launch.
|
||||
- **`tools/`** (file-watch-diff, squad-injector, exporters) — helpers for the FLE-Lua-bridge idea in
|
||||
`docs/direction.md`. Not part of the live FIFA 17 path.
|
||||
- **`docs/foundational-xi-injection-test.md`** — a planned (not executed) test procedure for the FLE
|
||||
bridge route.
|
||||
|
||||
## Stubbed / blocked (FIFA 23 lineage)
|
||||
|
||||
- **`openfut-bridge`** — in-process `version.dll` hook on ProtoSSL. Git history: injection works but
|
||||
the effort hit an "architectural wall" (async event-driven gate, not a poll). Superseded first by
|
||||
the FLE-bridge pivot, then by the FIFA 17 route. Its `CLAUDE.md` task list is historical.
|
||||
|
||||
## Unknown / requires investigation
|
||||
|
||||
- Whether the FIFA 17 hub supports actually **playing a FUT match** offline and getting results back.
|
||||
- Whether FUT actions beyond the now-verified pack reveal/assignment flow (submit SBC, transfer
|
||||
market buy/sell, matches) round-trip correctly through `utas_server.py`.
|
||||
- The exact division of FUT state ownership once Core is wired in (who is source of truth).
|
||||
- Degree of FIFA 23 wire-format identity — asserted ("identical wire format") but the FIFA 23 client
|
||||
has not been re-tested against these responders in this repo's evidence.
|
||||
|
||||
## Known technical debt / hazards
|
||||
|
||||
- **Root docs are stale.** `README.md`, `CLAUDE.md`, `openfut-bridge/CLAUDE.md` all describe FIFA 23
|
||||
as the target and mark FIFA 23 integration as the open item — they predate the FIFA 17 success.
|
||||
- **All host state is volatile** across reboot except the `/etc/hosts` line — re-run
|
||||
`openfut-fut.sh start`. Requires `ptrace_scope=0` + root arming (security-relevant).
|
||||
- **Whole stack rides on EAAC staying neutralized** and game updates being off; a client update can
|
||||
break the memory patches (VAs) and cert bypass.
|
||||
- **`fifa17-recon/tools/lsx_responder_v2.py` is currently modified in the working tree** (uncommitted).
|
||||
- `33068179` / `CAGE` remains the responder fallback, but the launcher now blocks one-button launch
|
||||
until an explicit persona is configured and synchronized across LSX, Blaze, POW, and UTAS.
|
||||
@@ -1,45 +0,0 @@
|
||||
# Python Retirement Readiness (post-P1)
|
||||
|
||||
Classification of every Python component still present. Evidence: prod-host
|
||||
`owner=` dispatch log (OBSERVED), AuxServiceMap/PythonContractMap scouts,
|
||||
container `entrypoint.sh`. **P2 rollback stays hot regardless** (do NOT remove
|
||||
rollback-to-python-p2.sh, :p2-rollback image b1b929953f, profile 39bb3e83).
|
||||
|
||||
## A. LIVE PRODUCTION REQUIRED (still owns behavior in the live flow)
|
||||
| Component | Role | Rust status | Retire when |
|
||||
|---|---|---|---|
|
||||
| oracle utas_server.py (:8199) NON-ECONOMY | **remaining**: user/club rename, non-active squad/<n>, draft/*, mode-gated leaderboards + /sbs/* | **Most non-economy migrated 2026-08-17** (account/sync, auth, userMassInfo, user, clientdata, hub, club/stats/*, settings, accountinfo, phishing, match/reset, leaderboards/options, watchList, static acks, item-defs (item/resource,defid), marketdata (+/pricelimits), and the flag-off empty reads season/tournament/champion/clubUser/user/list → `{}` — all Rust). See PRODUCTION_AUTHORITY_MATRIX | remaining tail is mutation (user/club), no-Core-model (squad/<n>), or unimplemented modes (draft/leaderboards/sbs) |
|
||||
| blaze_responder_v3b.py (:42130 Blaze) | FUT Blaze transport | Rust openfut-blaze-host COMPLETE, gate-proven (Gate 10) | container cutover (operator-gated deploy) |
|
||||
| blaze_responder_v3b.py (:42127 redirector TLS) | first-hop TLS redirect | Rust openfut-redirector-host COMPLETE (OpenSSL) | container cutover + cert consistency |
|
||||
| roster_server.py (:8081) | roster-update XML | Rust openfut-roster-host COMPLETE (unit-only) | container cutover |
|
||||
| pow_server.py (:8094/:8080) | Proof-of-Work / content | NO Rust crate; 58 templates, bodies un-reversed | needs Rust host + body RE (BLOCKED) |
|
||||
|
||||
## B. ORACLE ONLY (reference / differential, not production authority)
|
||||
- utas_server.py economy handlers: now oracle-only (economy is Rust; Python economy hits = 0). Used by
|
||||
`economy_differential.rs` as the parity oracle. KEEP.
|
||||
- fut_store.py / fut_accounts.py / fut_consumables.py: reference models + Ghidra-derived taxonomy source. KEEP.
|
||||
|
||||
## C. MIGRATION TOOL ONLY (offline)
|
||||
- scripts/*.py (seed_fifa17_cards.py, utas-observe/mutate/diff, check-*). KEEP (dev tooling).
|
||||
- fifa17-recon/tools/db_dump.py etc.: table extraction. KEEP.
|
||||
|
||||
## D. ROLLBACK ONLY (hot, DO NOT REMOVE)
|
||||
- rollback-to-python-p2.sh; image openfut-fut-backend:p2-rollback (b1b929953f) + :dev; profile 39bb3e83;
|
||||
compose/env backup; tuple OPENFUT_SERVERS="blaze roster utas pow".
|
||||
|
||||
## E. DEAD (advertised but not followed in current FIFA17 flow)
|
||||
- Nucleus /connect/token stub (:42131): 0 live hits across Gate 5-10 (client never POSTs). Keep the ADVERTISED
|
||||
URL so the client can't reach real EA, but no migration needed. No Rust listener required.
|
||||
- fifa-blaze (FIFA23 capture stub), openfut-bridge (FIFA23): retired lineage, not in FIFA17 flow.
|
||||
|
||||
## Retirement gating
|
||||
1. **Mostly DONE (2026-08-17)**: account/sync, ut/auth (Rust SID mint), userMassInfo (full), clientdata,
|
||||
club/stats/{country,league,team}, watchList, static acks, the flag-off empty reads
|
||||
(season/tournament/champion/clubUser/user/list → `{}`), item-defs (item/resource,defid), and
|
||||
marketdata (+/pricelimits, container-type-exact) migrated + deployed. **Remaining on Python**:
|
||||
user/club rename (mutating; needs a Core write), non-active squad/<n> (no multi-squad Core model),
|
||||
and the unimplemented modes draft/* + leaderboards + /sbs/* (need the mode logic ported, not a proxy).
|
||||
2. Deploy Rust blaze/roster/redirector via container cutover (operator-gated) — then those Python responders
|
||||
are class D/E only.
|
||||
3. POW: build Rust host + reverse bodies (largest blocker) OR keep Python POW as class A indefinitely.
|
||||
4. Only after a long stable window: consider retiring the P2 rollback (separate explicit decision — NOT now).
|
||||
@@ -1,293 +0,0 @@
|
||||
# SOLD-row A/B experiment — isolated FIFA-17 staging runbook
|
||||
|
||||
Operator procedure for driving the seller-facing **SOLD row** experiment with a real
|
||||
FIFA 17 client against a **completely separate** staging stack, while the frozen P1
|
||||
production stack keeps running untouched.
|
||||
|
||||
The experiment asks one question the PE cannot answer: for a `closed` trade-pile row,
|
||||
does the FUT ActionScript front end treat `bidState:"highest"` or `bidState:"buyNow"`
|
||||
as *"you sold this"*? Both are bit-identical to every native CardsDLL consumer, but
|
||||
`bidState` is published to the movie verbatim as `YOURBID`, so only the client can
|
||||
say. See `openfut-utas-host/src/sold_experiment.rs`.
|
||||
|
||||
---
|
||||
|
||||
## 1. What the two scripts do
|
||||
|
||||
| Script | Purpose |
|
||||
| --- | --- |
|
||||
| `scripts/sold-staging-up.py` | Brings up the **whole** staging stack (Core + utas-host + Blaze), seeds two identities, prints the client config block. One entry point. |
|
||||
| `scripts/sold-staging-down.py` | Stops **exactly** the processes the up script recorded, proves the staging ports are free, and proves production is still alive. |
|
||||
|
||||
```bash
|
||||
cd /home/alex/OpenFUT
|
||||
|
||||
# variant A — the sold row is closed / highest
|
||||
python3 scripts/sold-staging-up.py --variant highest
|
||||
|
||||
# variant B — same state, only bidState (and optionally coinsProcessed) differ
|
||||
python3 scripts/sold-staging-up.py --variant buyNow --coins-processed 1 \
|
||||
--count-mode active_plus_sold
|
||||
|
||||
# projection disabled: identical to production behaviour (control run)
|
||||
python3 scripts/sold-staging-up.py --variant off
|
||||
|
||||
python3 scripts/sold-staging-down.py # stop, keep dbs/logs as evidence
|
||||
python3 scripts/sold-staging-down.py --purge # stop and delete the staging dir
|
||||
```
|
||||
|
||||
To flip A → B, run `sold-staging-down.py` then `sold-staging-up.py --variant buyNow`.
|
||||
The staging databases are throwaway: a fresh `up` deletes the previous ones and
|
||||
reseeds, so each variant run starts from a known state. Never edit a live stack's
|
||||
environment in place — the banner in `logs/utas-host.log` is the only record of which
|
||||
variant produced a capture, and it is written at startup.
|
||||
|
||||
---
|
||||
|
||||
## 2. Staging port block and state
|
||||
|
||||
Everything lives under `/home/alex/openfut-sold-staging/` (override with `--dir` or
|
||||
`OPENFUT_SOLD_STAGING_DIR`).
|
||||
|
||||
| Service | Bind | Notes |
|
||||
| --- | --- | --- |
|
||||
| staging Core | `127.0.0.1:18081` | loopback only; the client never talks to Core |
|
||||
| staging utas-host | `0.0.0.0:8299` | the UTAS the client reaches |
|
||||
| staging Blaze redirector | `0.0.0.0:42327` | TLS; receives EA `:10041` and `:42230` |
|
||||
| staging Blaze main | `0.0.0.0:42330` | receives EA `:42127` |
|
||||
| staging Blaze nucleus | `0.0.0.0:42331` | local OAuth stub |
|
||||
| dead Python upstream | `127.0.0.1:8399` | **must stay unbound** (see §5) |
|
||||
|
||||
The redirector is on `42327`, not the "obvious" `42227`: that port is permanently
|
||||
held by `openfut-redirector-host` (pid 862419).
|
||||
|
||||
| State file | Path |
|
||||
| --- | --- |
|
||||
| Core sqlite | `staging-core.db` |
|
||||
| market sqlite | `staging-market.db` |
|
||||
| pile sqlite | `staging-pile.db` |
|
||||
| identity store | `staging-identity.json` |
|
||||
| clientdata blobs | `staging-clientdata.json` |
|
||||
| content pack | `content/fifa17-production-cards.json` |
|
||||
| identity catalog | `content/fifa17-production-catalog.json` |
|
||||
| patched Blaze | `blaze/blaze_responder_staging.py` |
|
||||
| process manifest | `manifest.json` |
|
||||
| logs | `logs/{core,utas-host,blaze,blaze-responder}.log` |
|
||||
|
||||
Seeded identities (direct SQL against the schema Core migrates for itself):
|
||||
|
||||
* **Seller A** — profile `prof-seller-a-cage`, club `club-seller-a-cage`, username
|
||||
`CAGE`, `game_id = fifa17`, 1 000 coins, 11 starters in a canonical squad, plus
|
||||
**one disposable item** (`owned-a-disposable`, card `fifa17_232273`, Nelson Atiagli
|
||||
LB 51) — that is the card to list and sell during the experiment.
|
||||
Seller A carries `game_id = fifa17`, so the retail client (which sends
|
||||
`X-OpenFUT-Game: fifa17`) resolves to this profile and can log in as persona
|
||||
`33068179`.
|
||||
* **Buyer B** — profile `prof-buyer-b`, club `club-buyer-b`, 20 000 coins, parked on
|
||||
its own `game_id = fifa17-buyer-b`. Core is single-profile-per-game, so this keeps
|
||||
Buyer B from ever shadowing Seller A as the active `fifa17` profile while still
|
||||
being reachable by club id for a settlement.
|
||||
|
||||
---
|
||||
|
||||
## 3. The client change — the only thing the operator touches
|
||||
|
||||
The FIFA client learns its **UTAS** base URL from Blaze, not from `openfut.cfg`:
|
||||
`blaze_responder_v3b.py` builds `UTAS_BASE = "http://%s:8099/" % _ADVERTISE` with the
|
||||
port **hardcoded**. The up script therefore copies the responder into the staging dir
|
||||
and rewrites that literal to `:8299`. **Pointing the client at staging Blaze is
|
||||
sufficient to move UTAS too** — there is no UTAS line in `openfut.cfg`.
|
||||
|
||||
On the FIFA client machine **10.10.0.105**, file
|
||||
`"/mnt/games/FIFA 17/openfut.cfg"`:
|
||||
|
||||
```bash
|
||||
# back it up FIRST
|
||||
cd "/mnt/games/FIFA 17"
|
||||
cp openfut.cfg openfut.cfg.prod
|
||||
```
|
||||
|
||||
Set these **three** lines:
|
||||
|
||||
```
|
||||
host=10.10.0.120
|
||||
blaze_redirector_port=42327
|
||||
blaze_main_port=42330
|
||||
```
|
||||
|
||||
Leave `https_port=8443` **unchanged** — that is Bridge, which holds no economy
|
||||
state. The resulting file is:
|
||||
|
||||
```
|
||||
host=10.10.0.120
|
||||
https_port=8443
|
||||
blaze_redirector_port=42327
|
||||
blaze_main_port=42330
|
||||
```
|
||||
|
||||
Then **relaunch the FIFA 17 client.** A client that is already running caches its
|
||||
UTAS session (SID) in memory and will not re-auth against a different stack; the
|
||||
symptom is the dialog *"An error occurred downloading the FUT Squad Update"* with
|
||||
**zero** requests in the staging host log. "The operator is at the main menu" is not
|
||||
the same as "the client disconnected".
|
||||
|
||||
### Revert to production
|
||||
|
||||
```
|
||||
host=10.10.0.120
|
||||
https_port=8443
|
||||
blaze_redirector_port=42127
|
||||
blaze_main_port=42130
|
||||
```
|
||||
|
||||
or simply `cp openfut.cfg.prod openfut.cfg` — then **relaunch the client again**.
|
||||
These are the values the file holds today; the staging scripts never write to
|
||||
10.10.0.105, so this revert is the *only* client-side change to undo.
|
||||
|
||||
---
|
||||
|
||||
## 4. Running the experiment
|
||||
|
||||
1. `python3 scripts/sold-staging-up.py --variant highest`
|
||||
2. Apply §3 to the client and relaunch it. Confirm it reaches the FUT hub;
|
||||
`logs/utas-host.log` should show `route=auth status=200 sid_opened=true`.
|
||||
3. List the disposable item on the transfer market from the client.
|
||||
4. Complete the sale (Buyer B side) through the real settlement path.
|
||||
5. Observe the seller's Transfer List: which bucket the row lands in, what the
|
||||
counter says, and which request the client issues to clear it. Capture both the
|
||||
screen and `logs/utas-host.log`.
|
||||
6. `python3 scripts/sold-staging-down.py`
|
||||
7. `python3 scripts/sold-staging-up.py --variant buyNow`, relaunch the client, and
|
||||
repeat steps 3–5. The two runs differ **only** in `bidState` (and
|
||||
`coinsProcessed`/`count` if those flags are used), which is what makes the client's
|
||||
reaction attributable.
|
||||
8. When done: `sold-staging-down.py` and revert the client per §3.
|
||||
|
||||
`DELETE /ut/delete/game/fifa17/trade/sold` clears the sold rows (logged as
|
||||
`route=market-clear-sold cleared=N`) — that is the verb the client is expected to
|
||||
issue from the *Clear Sold* affordance, and seeing whether it does is part of the
|
||||
observation.
|
||||
|
||||
---
|
||||
|
||||
## 5. Isolation: what guarantees production is untouched
|
||||
|
||||
Production frozen P1 must keep running throughout. The scripts enforce, not assume:
|
||||
|
||||
* **Hard port deny-list.** `8099 8199 18080 8443 42127 42130 42131 4216 8080 8081
|
||||
8094` are refused before every bind and before every HTTP request the scripts make.
|
||||
Each staging port is also proven free first, and bring-up aborts before launching
|
||||
anything if one is not.
|
||||
* **Hard path deny-list.** Every filesystem path goes through `safe_path()`, which
|
||||
refuses `/home/alex/openfut-promotion/state/` (the live `prod-core.db`,
|
||||
`prod-market.db`, `prod-pile.db`, `prod-identity.json`). Nothing there is ever
|
||||
opened — not even read. The FIFA17 content the staging stack loads is copied from
|
||||
`/home/alex/openfut-post-p1/staging/emit/content/`, a build tree outside that
|
||||
directory.
|
||||
* **The Python fallback fails closed.** `OPENFUT_UTAS_PYTHON_URL` points at
|
||||
`http://127.0.0.1:8399`, where **nothing listens**, so a fallback to the Python
|
||||
oracle surfaces as a visible connection error instead of silently serving the
|
||||
production oracle on `8199`.
|
||||
* **Binaries are copied into the staging dir and run from there.** A later
|
||||
`cargo build` cannot change what staging is running, and — more importantly — every
|
||||
staging process's `/proc/<pid>/cmdline` provably contains the staging directory,
|
||||
which production's never can.
|
||||
* **Teardown kills by recorded pid only, never by pattern.** `openfut-utas-host` and
|
||||
`openfut-core` each name *two* live processes on this machine. `sold-staging-down.py`
|
||||
reads pids from `manifest.json`, re-reads `/proc/<pid>/cmdline` and **refuses** to
|
||||
signal anything whose cmdline does not contain the staging dir; it refuses the known
|
||||
production pids explicitly; and it signals only the process group the up script
|
||||
created (`pgid == pid`, via a new session). There is no `pkill`/`pgrep` anywhere.
|
||||
* **The Blaze patch cannot silently no-op.** Six substitutions are applied to the
|
||||
copy, each anchored to a whole assignment line and each required to match **exactly
|
||||
once**; the file is then re-read from disk and every value re-checked, including an
|
||||
explicit assertion that `UTAS_BASE` no longer contains `:8099`. A responder that
|
||||
changed shape aborts bring-up rather than running half-patched and pointing at
|
||||
production. The four protocol values are `REDIR_PORT`, `BLAZE_PORT`,
|
||||
`NUCLEUS_PORT`, `UTAS_BASE`; the two extra ones are the responder's hardcoded
|
||||
`LOG = "/tmp/blaze_responder.log"` and `RXDIR = "/tmp/blaze_rx"`, redirected into
|
||||
the staging dir so a capture can never be ambiguous about which stack wrote it.
|
||||
* **Production liveness is asserted** at preflight, at the end of bring-up, and after
|
||||
teardown (`prod utas-host` pid 3631953, `prod Core` pid 3374264).
|
||||
* **Nothing on 10.10.0.105 is written.** The scripts only *print* the config block;
|
||||
the operator edits it by hand.
|
||||
|
||||
### Production services deliberately shared, read-only
|
||||
|
||||
Staging Blaze advertises the **production** auxiliary endpoints, exactly the strings
|
||||
production Blaze advertises:
|
||||
|
||||
| Advertised to client | Value | Why sharing is safe |
|
||||
| --- | --- | --- |
|
||||
| roster XML / roster URL | `10.10.0.120:8081` | static roster update XML; **no economy state**. Hardcoded as `ROSTER_HOST = "%s:8081" % _ADVERTISE` in the responder. |
|
||||
| `FIFA_POW_CONTENT_SERVER_URL` | `10.10.0.120:8085` | static POW content; no economy state (`POW_CONTENT_HOST`) |
|
||||
| `FIFA_POW_URL` / nucleus proxy | `10.10.0.120:8094` | POW API, only emitted when `FUT_POW=1`, which staging leaves unset (`POW_HOST`) |
|
||||
| LSX layer-1 responder | `:4216` | persona/session handshake for the same persona `33068179`; no economy state |
|
||||
|
||||
The staging stack **never connects** to any of these itself — it only hands the client
|
||||
the same strings production hands it. Every service that owns economy state (UTAS,
|
||||
Core, market/pile/identity) is duplicated on staging ports against staging files.
|
||||
Because those aux services are shared, **do not run a staging session and a
|
||||
production session simultaneously**: one client at a time.
|
||||
|
||||
`heat2` (TDF codec) and `fut_account` (the single source of truth for persona
|
||||
`33068179` / `CAGE`, shared with LSX and the oracle) are imported read-only from
|
||||
`fifa17-recon/tools/` via `PYTHONPATH` rather than copied, so the staging Blaze
|
||||
asserts a **byte-identical** identity to what LSX already asserts. Cross-layer
|
||||
identity consistency is the constraint; a divergent copy would break login.
|
||||
|
||||
---
|
||||
|
||||
## 6. Verifying a live stack by hand
|
||||
|
||||
All read-only, and none of it touches a production port:
|
||||
|
||||
```bash
|
||||
S=/home/alex/openfut-sold-staging
|
||||
|
||||
# which variant is live
|
||||
grep sold-experiment $S/logs/utas-host.log
|
||||
|
||||
# staging UTAS answers, from staging state
|
||||
python3 - <<'PY'
|
||||
import http.client, json
|
||||
c = http.client.HTTPConnection("127.0.0.1", 8299, timeout=5)
|
||||
c.request("GET", "/ut/game/fifa17/tradePile/counts",
|
||||
headers={"X-OpenFUT-Game": "fifa17"})
|
||||
print(c.getresponse().read().decode())
|
||||
PY
|
||||
|
||||
# the patched Blaze points at staging UTAS, not production
|
||||
grep -nE '^(UTAS_BASE|REDIR_PORT|BLAZE_PORT|NUCLEUS_PORT) = ' \
|
||||
$S/blaze/blaze_responder_staging.py
|
||||
|
||||
# exactly six lines differ from the original responder
|
||||
diff fifa17-recon/tools/blaze_responder_v3b.py \
|
||||
$S/blaze/blaze_responder_staging.py
|
||||
|
||||
# no staging process has a handle on production state
|
||||
for p in $(python3 -c "import json;print(' '.join(str(x['pid']) for x in json.load(open('$S/manifest.json'))['processes']))"); do
|
||||
echo "pid $p: $(sudo ls -l /proc/$p/fd | grep -c openfut-promotion/state) prod-state handles"
|
||||
done
|
||||
|
||||
# production is still up
|
||||
ps -o pid=,cmd= -p 3631953
|
||||
```
|
||||
|
||||
Never `curl` port 8099/8199/18080 to "compare with production" — those ports are on
|
||||
the deny-list. Independence is proven structurally (separate pids, separate ports,
|
||||
separate files, zero shared handles), not by poking the live stack.
|
||||
|
||||
---
|
||||
|
||||
## 7. Troubleshooting
|
||||
|
||||
| Symptom | Cause / fix |
|
||||
| --- | --- |
|
||||
| `REFUSING to start -- these staging ports are not free` | something else holds a staging port. Nothing was launched. Free it, or edit the port block at the top of `sold-staging-up.py`. |
|
||||
| `a previous staging stack is STILL UP` | run `sold-staging-down.py` first. The up script never steps on a live stack. |
|
||||
| `blaze patch <NAME> applied 0 times` | `blaze_responder_v3b.py` changed shape. Fix the regex in `blaze_patches()`; do **not** disable the assertion. |
|
||||
| `seed card ids are not resolvable` | the emitted content in `CONTENT_SRC` no longer contains a seeded `card_id`. Re-point `CONTENT_SRC` or update the seed ids — both memberships (content pack **and** identity catalog) are required. |
|
||||
| client shows *"error occurred downloading the FUT Squad Update"*, staging host log shows **no request at all** | the failure is upstream of UTAS. Almost always a client that was not relaunched after the config change (§3). Check the *absence* of requests in `logs/utas-host.log` before touching any code. |
|
||||
| `<name> pid N is alive but its cmdline does NOT contain <staging dir>` | pid reuse or a stale manifest. Teardown refuses rather than guessing — identify the process by hand and stop it by pid. |
|
||||
| staging host log shows connection errors to `127.0.0.1:8399` | expected and intended: a UTAS route fell through to the Python fallback, which is deliberately dead. Fix the route; do **not** point the fallback at `8199`. |
|
||||
@@ -1,250 +0,0 @@
|
||||
# OpenFUT — Direction Document
|
||||
*The pivot: FUT lives in the app; FIFA 23 is the match renderer.*
|
||||
*Supersedes the Blaze-backend approach as the primary plan. Last updated 2026-06-30.*
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal (revised)
|
||||
|
||||
Deliver an **intuitive way to play a FUT-style experience with FIFA 23**, where:
|
||||
|
||||
- The entire **FUT experience** — cards, squads, packs, SBCs, coins, chemistry,
|
||||
progression — lives in a **custom app** (web UI or desktop) built on the
|
||||
already-complete OpenFUT Core economy backend.
|
||||
- **FIFA 23 is demoted to a match renderer.** Its only job is to play a
|
||||
single-player match using the squad the app built. No FUT mode, no online, no
|
||||
Blaze, no EA servers.
|
||||
|
||||
This deliberately drops in-game FUT cards/UI (they live in the app) in exchange
|
||||
for a project that **converges** instead of being gated behind months of
|
||||
backend reverse-engineering.
|
||||
|
||||
### Why this replaces the backend plan
|
||||
|
||||
The status review confirmed the backend route (faking EA's online stack) is
|
||||
blocked at an upstream in-process EbisuSDK gate, with Blaze/Fire2 unconfirmed
|
||||
beyond it — realistically 3–6 months of expert RE that may not converge. The
|
||||
app-centric route sidesteps **every** wall in that review by never making FIFA's
|
||||
own FUT mode run.
|
||||
|
||||
---
|
||||
|
||||
## 2. Base mode: Career, not Kick-Off
|
||||
|
||||
**Career mode is the base.** Reasons:
|
||||
|
||||
- FLE's live-editing API (`EditDBTableField`, Freeze Lineup) is **confirmed to
|
||||
work in career mode** and explicitly does NOT work in FUT/online modes.
|
||||
- Career already provides the FUT-shaped scaffolding we'd otherwise fake:
|
||||
persistent club, a fixture schedule, recorded results, progression across a
|
||||
season.
|
||||
- **Match results are written into the career DB**, making result capture a DB
|
||||
read rather than a fragile live-memory grab.
|
||||
|
||||
**Kick-Off is the prototype sandbox.** Use it first to prove squad injection
|
||||
works with nothing to corrupt (no save to break), then move the real loop onto
|
||||
career. Run the foundational injection test in BOTH.
|
||||
|
||||
---
|
||||
|
||||
## 3. Core architecture: the bidirectional FLE bridge
|
||||
|
||||
The backbone is a **bidirectional channel between the app and a resident FLE Lua
|
||||
script running inside the game.** Everything else is messages over this channel.
|
||||
|
||||
```
|
||||
Custom App (FUT experience)
|
||||
│ squad push ──────────────► ┌─────────────────────────────┐
|
||||
│ │ Resident FLE Lua script │
|
||||
│ ◄────────── game state │ (inside FIFA 23, career) │
|
||||
│ ◄────────── match result │ - reads game state │
|
||||
└────────────────────────────► │ - applies squad live │
|
||||
(file-watch or local socket) │ - reads results from DB │
|
||||
└─────────────────────────────┘
|
||||
│
|
||||
FIFA 23 plays the match
|
||||
```
|
||||
|
||||
Three message types over the bridge:
|
||||
|
||||
1. **App → Game: squad push.** The app's chosen XI + stats applied LIVE via
|
||||
`EditDBTableField`, replicating whatever DB write FLE's "Freeze Lineup"
|
||||
feature performs (see `docs/foundational-xi-injection-test.md` — the exact
|
||||
field(s) are found by diffing, not assumed). No restart, no
|
||||
file-copy-reload. (File-load remains a fallback.)
|
||||
|
||||
2. **Game → App: game state.** The resident script polls the game's current
|
||||
screen/menu state and reports "safe to apply" vs "not safe", driving a smart
|
||||
Apply button in the app (see §5).
|
||||
|
||||
3. **Game → App: match result.** After full-time, the script reads the result
|
||||
from the career DB and pushes score/scorers to the app, which awards
|
||||
coins/progression. (Manual entry is the baseline fallback.)
|
||||
|
||||
The bridge transport can be a watched file the in-game Lua polls, or a local
|
||||
socket — decided in build (see §7). Either way the *game keeps running*; a file,
|
||||
if used, is just the message channel, not a reload.
|
||||
|
||||
---
|
||||
|
||||
## 4. Tiered mod scope
|
||||
|
||||
Build in tiers matched to risk. The core tier is all the SAME kind of DB write,
|
||||
so it lands together once squad injection works.
|
||||
|
||||
### Tier 1 — Core writes (ride the same live DB-edit mechanism)
|
||||
- **Squad / custom XI** — the load-bearing primitive (Freeze Lineup's
|
||||
underlying write, replicated via script — see §6).
|
||||
- **Player stats as "cards"** — card tiers, in-form versions, SBC upgrades all
|
||||
expressed as written attribute values.
|
||||
- **Chemistry as stat adjustment** — app computes FUT chemistry, applies it as
|
||||
small stat bumps when writing players in (no in-game chem UI; that's in the app).
|
||||
- **Appearance / identity** — kits, names, team assignment, so the club looks
|
||||
like your club on the pitch.
|
||||
- **Formation / tactics** — squad structure carries the app's build onto the pitch.
|
||||
|
||||
### Tier 2 — Confirm-then-add
|
||||
- **Match difficulty per game** — to drive a Squad-Battles-style "this opponent is
|
||||
World Class". Settable in-game trivially; programmatic drive needs confirming.
|
||||
- **Match rules / modifiers** (half length, etc.) — for app-defined challenges.
|
||||
|
||||
### Tier 3 — Result capture (manual baseline + automated stretch)
|
||||
- **Manual:** user enters the score in the app after the match. Zero RE, ships
|
||||
first.
|
||||
- **Automated:** resident script reads the career-DB result (or, for Kick-Off,
|
||||
reads the in-match score from memory at full-time — precedent exists: the
|
||||
CM cheat table's `export_season_stats.lua` already reads goals/cards from
|
||||
memory via known offsets). Push to app → auto-award progression.
|
||||
|
||||
### Out of scope (stays in the app, by design)
|
||||
- In-game FUT cards, FUT menus, pack-opening animation, chemistry board, FUT
|
||||
presentation. The app is where it looks/feels like FUT.
|
||||
|
||||
---
|
||||
|
||||
## 5. The smart Apply button (state-aware)
|
||||
|
||||
Live DB edits only "stick" in safe menu states (the in-game "Edit Player" screen,
|
||||
for example, overwrites edits). So the bridge reads game state and gates applying:
|
||||
|
||||
- Resident Lua script polls the game's current-screen value (a few Hz),
|
||||
classifies **safe / not safe**, reports to the app.
|
||||
- App's **Apply button is enabled only when the script confirms a safe state**
|
||||
(squad hub, main menu); greyed otherwise.
|
||||
- **Safe-by-default-OFF:** unknown state → button greyed → never a risky write.
|
||||
Expand the known-safe list incrementally as states are confirmed.
|
||||
- **v2 (more seamless):** instead of greying, the app always lets you click and
|
||||
the script **queues** the apply, executing the moment a safe state is entered,
|
||||
then confirms back. Greying is v1; queue-and-apply is v2.
|
||||
|
||||
`IsInCM()` is a confirmed state-read; the specific screen-state address + the
|
||||
value→screen mapping is one-time reconnaissance (same technique as result reading).
|
||||
|
||||
---
|
||||
|
||||
## 6. What's confirmed vs what needs validating
|
||||
|
||||
**Confirmed (from FLE's own Lua API docs/wiki, checked 2026-06-30):**
|
||||
- FLE live-edits the running career DB without restart, via `EditDBTableField`
|
||||
(real signature: `EditDBTableField(cell)` where `cell = row["fieldname"]`
|
||||
with `.value` mutated first — not the table/index/field/value form an
|
||||
earlier draft of this doc assumed).
|
||||
- FLE reads game state via `IsInCM()`.
|
||||
- A `MEMORY` Lua class exists (`ReadInt`/`WriteInt`/`ReadMultilevelPointer`/
|
||||
etc.) for arbitrary process memory — confirms the result-reading fallback
|
||||
in §4 Tier 3 is a real, documented capability, not just cheat-table analogy.
|
||||
- `GetPlayersStats()` is a documented function returning per-player
|
||||
goals/assists/cards/etc. — a better confirmed path for match-result capture
|
||||
than raw memory offsets.
|
||||
- **Freeze Lineup** (Formation Editor → arrange XI → tick "Freeze Lineup" →
|
||||
`Data → Save`) is FLE's actual documented mechanism for forcing a starting
|
||||
XI in career mode. This **replaces** "selection bias" below.
|
||||
- OpenFUT Core (economy) is complete and tested.
|
||||
|
||||
**Walked back — not actually confirmed:**
|
||||
- "Selection bias forces specific players into the starting XI" — no such
|
||||
field appears anywhere in FLE's documented Lua API or its own example
|
||||
scripts. This was an unverified assumption carried over from general FIFA
|
||||
modding precedent (other titles), not anything checked against FLE/FIFA 23.
|
||||
See `docs/foundational-xi-injection-test.md` for the corrected plan, which
|
||||
uses Freeze Lineup instead.
|
||||
|
||||
**Needs validating (the foundational tests — see §7):**
|
||||
- Whether Freeze Lineup actually holds into a played match (FLE's wiki
|
||||
documents the feature but not a live-match test of it).
|
||||
- What DB table/field Freeze Lineup's `Data → Save` actually writes — it's
|
||||
GUI-only and undocumented at that level; finding it is part of the
|
||||
foundational test.
|
||||
- Whether that write can be replicated by a script (`EditDBTableField`) well
|
||||
enough to drive it from an EXTERNAL trigger, not just the Formation Editor
|
||||
UI — required for the app↔game bridge.
|
||||
- The app↔game bridge transport (file-watch vs socket) works cleanly under the
|
||||
run setup.
|
||||
- The screen-state address + safe/not-safe classification (FLE's `Events`
|
||||
API page exists in the wiki index but its content is currently empty/
|
||||
undocumented — this is more open than previously assumed).
|
||||
- Result read-back from the career DB after a match.
|
||||
|
||||
**Standing caveat:** the whole stack rides on **EAAC staying neutralized**
|
||||
(FLE's fake-launcher bypass). If a game update re-enables it, hooks fail. Keep
|
||||
game updates off; confirm neutralized state each session.
|
||||
|
||||
---
|
||||
|
||||
## 7. Build order / next steps
|
||||
|
||||
Each is a bounded, verifiable step. Do them in order; later ones depend on
|
||||
earlier answers.
|
||||
|
||||
1. **FOUNDATIONAL TEST — live custom XI in career.** Confirm Freeze Lineup
|
||||
holds into a played match, reverse-engineer the DB write it makes, then
|
||||
replicate that write from a script so it can be triggered externally
|
||||
instead of through the Formation Editor UI. See
|
||||
`docs/foundational-xi-injection-test.md` for the full procedure. *Done =
|
||||
a script-driven write produces a match that fields the squad you
|
||||
specified.* Everything rests on this.
|
||||
|
||||
2. **Pick the bridge transport.** Decide file-watch vs local socket for app↔game
|
||||
messaging; implement the minimal app→game squad push. *Done = app sends a
|
||||
squad, the resident script receives and applies it.*
|
||||
|
||||
3. **Game-state reader + smart Apply.** Find the screen-state address, classify
|
||||
safe/not-safe, expose to the app, gate the Apply button. *Done = button greys
|
||||
when you enter a match/edit screen, enables in the squad hub.*
|
||||
|
||||
4. **Result read-back.** Read the career-DB match result post-game, push to app,
|
||||
award progression. Manual entry ships alongside as the fallback. *Done = app
|
||||
updates coins from a played match.*
|
||||
|
||||
5. **Tier 1 breadth.** Extend the squad push to carry stats, appearance,
|
||||
formation (same write mechanism). *Done = the club looks and plays like the
|
||||
app's build.*
|
||||
|
||||
6. **Tier 2 + economy loop polish.** Difficulty drive, challenges, and the full
|
||||
pack → SBC → squad → match → reward loop closed end-to-end.
|
||||
|
||||
### Decision still open
|
||||
- **App form factor:** web UI vs desktop app. This affects the bridge transport
|
||||
(a desktop app can hold a local socket more naturally; a web UI leans toward a
|
||||
small local helper/file-watch). Decide before step 2.
|
||||
|
||||
---
|
||||
|
||||
## 8. Provenance
|
||||
|
||||
Clean-room throughout. This route relies on FLE's documented public API and the
|
||||
game's own supported career mode — no EA backend, no Blaze, and nothing derived
|
||||
from leaked EA source. The earlier backend RE remains clean-room and is preserved
|
||||
as a spec artifact; it is simply no longer the primary path.
|
||||
|
||||
---
|
||||
|
||||
## 9. One-paragraph summary
|
||||
|
||||
OpenFUT becomes a **FUT companion app that uses FIFA 23 as a match engine.** The
|
||||
app owns the entire FUT experience; a resident FLE Lua script in career mode
|
||||
applies the app's squad live (no restart), reports game state to drive a safe
|
||||
Apply button, and reads match results back to feed progression. This sidesteps
|
||||
every backend wall, runs on confirmed FLE capabilities, builds on the finished
|
||||
economy core, and delivers the intuitive, offline, FUT-flavored loop that is the
|
||||
actual goal.
|
||||
@@ -1,443 +0,0 @@
|
||||
# FIFA 17 — Empty "My Packs" Client Contract (store/purchasegroup)
|
||||
|
||||
> **STATUS (2026-08-13): ROOT CAUSE ESTABLISHED; P2 backend compatibility workaround
|
||||
> IMPLEMENTED.** Root cause: FIFA 17's Store/Scaleform path resolves the `mypacks`
|
||||
> category even with zero unopened packs, and CardsDLL `FUN_1800147f0` assumes the
|
||||
> resolved group is non-null (crash if absent). Backend decision: **P2** — emit an
|
||||
> **active** non-openable synthetic `mypacks` placeholder (id 65534) when
|
||||
> `unopenedPackIds == []` (`fifa17-recon/tools/utas_server.py` `store_catalog`; tests
|
||||
> `fifa17-recon/tools/test_empty_mypacks.py`). Crash-safe + economy-safe; known UX
|
||||
> limitations (fake tile, click-dialog, Browse→My-Packs nav quirk) are Scaleform-driven
|
||||
> and require the client-side fix in `docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md`.
|
||||
> This is a FIFA-17-specific compatibility shim, NOT an EA-authentic representation,
|
||||
> and is confined to the FIFA-17 adapter/backend (NOT OpenFUT Core).
|
||||
|
||||
Evidence labels: **OBSERVED** (runtime capture / crash dump / already-decompiled RE
|
||||
quoted in-repo), **INFERRED**, **HYPOTHESIS**, **UNKNOWN**. No server behavior is
|
||||
changed by this document; it is analysis only.
|
||||
|
||||
Binaries (hashes verified 2026-08-13 on `.105`):
|
||||
`CardsDLL_Win64_retail.dll` SHA-256 `4706a881ae1fc7b5769fd810b25a868d29d2b16a8e65a7513436327ef645573c`
|
||||
(load base in the crash dump `0x00006FFFFC120000`; RE-space base `0x180000000`).
|
||||
`FIFA17.exe` SHA-256 `29c31cef12b0c3c2a7305220617c7b4fa139ab76b8c857851bdbe88987962899` (packed).
|
||||
|
||||
## 1. Question
|
||||
|
||||
How must the server represent an account that owns **zero unopened packs** in
|
||||
`GET /ut/game/fifa17/store/purchasegroup/all?ppInfo=true` so that FIFA 17 neither
|
||||
(a) crashes nor (b) shows "The pack you've selected is currently not available",
|
||||
**without granting the user a real/openable pack** and without breaking the normal
|
||||
bronze/gold/special store? The current OpenFUT answer (a synthetic inactive `mypacks`
|
||||
sentinel) only downgrades a crash to a dialog; it is not the correct contract.
|
||||
|
||||
## 2. Established experimental behavior (OBSERVED)
|
||||
|
||||
Profile-state ladder, all with normal packs 1/5/6/7 unchanged:
|
||||
|
||||
| profile `unopenedPackIds` | `mypacks` group in response | client outcome |
|
||||
|---|---|---|
|
||||
| `[]` (baseline) | one **inactive** sentinel pack `65534`, empty description | "pack not available" dialog → FUT Hub |
|
||||
| `[70]` (Exp A) | one **active** real owned pack `70` | **store works**; My Packs visible |
|
||||
| `[]` + sentinel suppressed (Exp B) | **no `mypacks` group at all** | **client CRASH** |
|
||||
|
||||
Captures: `store_purchasegroup_capture_2026-08-12.json` (baseline),
|
||||
`…_mypacks70_2026-08-12.json` (A), `…_empty_no_sentinel_2026-08-12.json` (B).
|
||||
Details in `STORE_TILE_6C.md` §14-§15.
|
||||
|
||||
## 3. Existing RE evidence (from `docs/plan-2026-08-05-store-subsystem.md`, decompiled)
|
||||
|
||||
All addresses RE-space (CardsDLL base `0x180000000`):
|
||||
- **`FUN_18013af30`** — per-element pack deser; each `purchase[]` entry → a `0x158`
|
||||
wire record. `displayGroup.value`(atom 0x377) → record `+0x00` (ctor default the
|
||||
literal `"unknown"`). `displayGroup.priority`(0x250) → `+0x34`.
|
||||
(store-subsystem §"wire record", :996.)
|
||||
- **`FUN_1800150d0`** — group builder. Walks `purchase[]` in array order; for each
|
||||
pack, finds-or-creates a `0x108` display group by **exact strcmp of
|
||||
`displayGroup.value` against `group+0x70`** (`FUN_180014380`). New group
|
||||
(`FUN_180012950`): `+0x00` = 1-based ordinal (groups-so-far+1), `+0x100` =
|
||||
priority, **`+0x104` = (value == "mypacks")**, `+0x40` = vector of `0x1a8` tile
|
||||
models. Pack→tile via `FUN_18002c3c0`. (:152-161, :1042-1049.)
|
||||
- **`FUN_1800147f0`** — group **resolver/renderer**, called as
|
||||
`FUN_1800147f0(model, screen+0x290, dataProvider, 0, 0)` from `FUN_18007dab0`.
|
||||
`screen+0x290 == 0` → "list the group tiles" (`FUN_180014610`); **any other value
|
||||
→ `FUN_180014420`, which exact-matches `group+0x00` (the ordinal) and returns NULL
|
||||
on a miss, after which `FUN_1800147f0` dereferences `[RAX+0x40]` with NO guard**
|
||||
("checked in raw disassembly … a real absence"). **"The only legal category values
|
||||
are 0 and the ordinals 1..N."** (:163-169, :1051-1054.)
|
||||
- **`FUN_180014580`** — the six-tab bar: switch 0..5 over the hardcoded lowercase
|
||||
literals `mypacks, points, bronze, silver, gold, special`. `FUN_18007e5e0` gives
|
||||
each panel a `PANEL_ID` = the matching group's ordinal, **or HIDES the panel** if no
|
||||
matching group; `FUN_18007df60` publishes `MYPACK_/…/SPECIAL_CATEGORY_ID`.
|
||||
(:202-206, :1058-1062.)
|
||||
|
||||
## 4. Store parser code path (OBSERVED, decompiled)
|
||||
|
||||
```
|
||||
HTTP 200 {"purchase":[...]} (server: store_catalog / _pack_body)
|
||||
→ per-element deser FUN_18013af30 → 0x158 wire records
|
||||
→ FUN_1800150d0 → 0x108 display groups (by displayGroup.value),
|
||||
tiles (0x1a8, FUN_18002c3c0) into group+0x40
|
||||
→ render FUN_18007dab0 → FUN_1800147f0(model, screen+0x290, …)
|
||||
screen+0x290 == 0 → FUN_180014610 list this group's tiles
|
||||
screen+0x290 == N>0 → FUN_180014420 exact-match ordinal; NULL on miss
|
||||
→ [RAX+0x40] dereference (NO NULL GUARD) ← crash site
|
||||
```
|
||||
|
||||
## 5. My Packs group construction (OBSERVED)
|
||||
|
||||
A `mypacks` group exists **iff at least one `purchase[]` entry carries
|
||||
`displayGroup.value == "mypacks"`** (the group is derived from packs; there is no
|
||||
independent group object on the wire). Its ordinal is its 1-based creation position
|
||||
in array order; `group+0x104` is set because the value is `"mypacks"`; its tiles live
|
||||
in `group+0x40`. Consequently the server **cannot** emit an "empty `mypacks` group"
|
||||
via `purchase[]` — removing the pack removes the group entirely.
|
||||
|
||||
## 6. Default / selected group logic (partly UNKNOWN)
|
||||
|
||||
- `FUN_1800147f0` resolves whatever category ordinal it is handed via `screen+0x290`;
|
||||
legal values are `0` (list tiles) and `1..N` (existing ordinals). A value that is
|
||||
not an existing ordinal (e.g. `-1` for a hidden/absent panel) → `FUN_180014420`
|
||||
NULL → crash. (OBSERVED via §8 crash + RE.)
|
||||
- **Whether the store defaults to / auto-resolves the `mypacks` category on open, and
|
||||
why it does so even when the unopened count is 0, is UNKNOWN** — the store-screen
|
||||
controller and default-tab selection are Scaleform/packed-FIFA17.exe
|
||||
("Which tile the movie thinks you clicked | CLIENT | Scaleform, unread",
|
||||
store-subsystem :695). Experiment B proves that in this configuration the client
|
||||
DID resolve a `mypacks` ordinal that did not exist (it crashed), so the store is
|
||||
reaching the My Packs category with zero owned packs. Root of "why" = UNKNOWN.
|
||||
|
||||
## 7. Pack availability predicate (UNKNOWN)
|
||||
|
||||
The predicate that turns the baseline inactive sentinel into "pack not available"
|
||||
(while active pack 70 passes) is **in the packed FIFA17.exe and unread**
|
||||
(`plan-2026-08-05-pack-opening.md:553`: `state/saleType/quantity/purchaseLimit/
|
||||
purchaseCount/start/end` are parsed and copied to the tile, "the predicate that greys
|
||||
a tile is in the packed exe and unread"). Candidate deciding fields, from the
|
||||
baseline↔A diff (INFERRED, unproven): **`state` (`inactive`→`active`)** and/or
|
||||
**`unopened` (`false`→`true`)**. The exact field is **UNKNOWN**.
|
||||
|
||||
## 8. Experiment B crash analysis (OBSERVED)
|
||||
|
||||
Minidump `CrashDump_…18.21.08…dmp` (the only crash in the hour; minute `:21` matches
|
||||
the `00:21:07` store request; SHA-256 `fbddda18…`), parsed:
|
||||
- Exception: **`0xC0000005` ACCESS_VIOLATION**, access type **READ**, **faulting VA
|
||||
`0x0000000000000048`**.
|
||||
- Faulting instruction: **`CardsDLL_Win64_retail.dll + 0x14882`** → RE-space
|
||||
**`0x180014882`** = **`0x92` bytes into `FUN_1800147f0`** (entry `0x1800147f0`).
|
||||
- Interpretation (OBSERVED crash ⋂ decompiled RE): the group pointer returned by
|
||||
`FUN_180014420` was **NULL** (no `mypacks` group present with `unopenedPackIds==[]`
|
||||
and the sentinel suppressed), and `FUN_1800147f0` dereferenced `[NULL+0x48]` → read
|
||||
of address `0x48` → access violation. This is exactly the "no null guard" branch
|
||||
the RE flagged (RE said `[RAX+0x40]`; the actual faulting offset is `+0x48`, same
|
||||
member region — the group struct's `+0x40` vector accessed via a `+0x48` field).
|
||||
- Note: the pre-built Ghidra project and `/tmp/fut/cardsdll.dll` were absent on `.105`
|
||||
(tmp cleared); confirmation used the OBSERVED crash dump + the previously-decompiled
|
||||
RE rather than a fresh (expensive) re-analysis. CardsDLL is unpacked, so this code
|
||||
is statically readable if a fresh project is ever needed.
|
||||
|
||||
## 9. Empty My Packs client contract (the answer, as far as evidence allows)
|
||||
|
||||
- **The client has NO guard for a missing selected group.** If the store resolves the
|
||||
`mypacks` category and no `mypacks` group exists, it null-derefs and crashes.
|
||||
(OBSERVED.)
|
||||
- **A `mypacks` group can only exist if a `purchase[]` entry carries
|
||||
`displayGroup.value=="mypacks"`.** (OBSERVED.) There is no wire representation of an
|
||||
"empty group."
|
||||
- **If the `mypacks` group's selected pack is not a valid/active pack, the client
|
||||
shows "pack not available".** (OBSERVED baseline vs A; deciding field UNKNOWN, §7.)
|
||||
- Therefore, under the *current* client behavior, a zero-unopened-packs account is
|
||||
only cleanly handled when the `mypacks` group contains a **valid active pack**
|
||||
(Exp A). Whether an active-but-non-openable placeholder would also satisfy the
|
||||
availability predicate is **UNKNOWN** (depends on §7).
|
||||
- **The correct retail-EA representation of zero unopened packs is UNKNOWN.** It is
|
||||
NOT "omit the group" (crash) and NOT "inactive placeholder" (dialog). It is most
|
||||
likely one of: (i) the retail store does not auto-select My Packs when the unopened
|
||||
count is 0 (a client/Scaleform decision, possibly gated by a count the server sets),
|
||||
or (ii) retail sends a `mypacks` entry the client treats as an empty-but-valid state
|
||||
via a field we have not identified. Neither is established.
|
||||
|
||||
## 10. Candidate server representations
|
||||
|
||||
| # | Candidate | Client evidence | Expected behavior | Confidence | Safe to test? |
|
||||
|---|---|---|---|---|---|
|
||||
| A | No `mypacks` pack, no `mypacks` group | Exp B crash (`0x180014882`, `[NULL+0x48]`) | **CRASH** | OBSERVED | Already tested — reject |
|
||||
| B | `mypacks` group present but zero packs | Not representable via `purchase[]` (groups derive from packs, `FUN_1800150d0`) | UNKNOWN | INFERRED-not-representable | No server mechanism |
|
||||
| C | Inactive placeholder pack (current sentinel) | Baseline dialog | "pack not available" → Hub | OBSERVED | Already tested — reject |
|
||||
| C′ | **Active** placeholder pack, id absent from `PACK_CATALOG` (so open/buy handlers reject it) | Exp A shows an *active* mypacks pack works; sentinel id 65534 is already rejected by open/buy (not in `PACK_CATALOG`) | Group resolves (no crash); MIGHT pass availability (no dialog) while remaining non-openable → no free pack | HYPOTHESIS | Yes — code change, restart; economy-safe (non-openable) |
|
||||
| D | Hide My Packs when unopened count == 0 | `FUN_18007e5e0` hides a panel with no group, but the store still resolved `mypacks` at count 0 (Exp B crash) | Hiding via absence CRASHES; a client count-gate is Scaleform/unknown | UNKNOWN | Not server-controllable as far as known |
|
||||
| E | Different default category when count == 0 | Default-tab selection is Scaleform/packed | UNKNOWN | UNKNOWN | Not server-controllable as far as known |
|
||||
| F | A count/quantities field (e.g. `ut/v2/store` `FutStorePackQuantities`, or `userInfo.unopenedPacks`) that suppresses the My Packs auto-select | eligibility gate exists (`ENDPOINT_MAP.md:60`); relationship to My Packs default UNKNOWN | UNKNOWN | HYPOTHESIS | Read-only RE first |
|
||||
|
||||
## 11. Recommended next controlled experiment
|
||||
|
||||
**Experiment C′ (economy-safe placeholder).** Keep `unopenedPackIds == []`; change the
|
||||
synthetic sentinel `65534` ONLY in `state` (and, if needed, `unopened`) so the
|
||||
`mypacks` group's single tile is **active** — but leave its id `65534` **absent from
|
||||
`PACK_CATALOG`** so `store_buy`/`open_pack`/`consume_unopened_pack` still reject it
|
||||
(no pack can be opened → **no free pack, no economy change**). Observe whether the
|
||||
store then opens without the "pack not available" dialog (would identify `state` as
|
||||
the availability field and give an economy-safe fix), or still shows the dialog
|
||||
(implicating another field / packed predicate).
|
||||
- Requires a temporary code change to `store_catalog` (sentinel construction) →
|
||||
restart. Same experiment discipline as Experiment B (patch container copy, capture,
|
||||
revert, restart). Economy-safe because the placeholder remains non-openable.
|
||||
- If C′ still fails, escalate to read-only RE of the availability predicate / the
|
||||
count-gated default-tab hypothesis (candidate F) before any further change.
|
||||
|
||||
## 12. Open questions
|
||||
|
||||
1. Exact field that flips the sentinel from "pack not available" to acceptable
|
||||
(`state`? `unopened`? another). UNKNOWN — packed predicate. (Exp C′ targets this.)
|
||||
2. Why does the store resolve/select `mypacks` with zero owned packs? Is there a
|
||||
server-settable count that would stop it? UNKNOWN — Scaleform/packed.
|
||||
3. Does retail FIFA 17 ever present an empty My Packs, and how? No capture on record.
|
||||
4. Is an active-but-non-openable placeholder (C′) accepted by the availability
|
||||
predicate? HYPOTHESIS — untested.
|
||||
|
||||
---
|
||||
|
||||
## Permanent-fix requirements (Phase 11 — REPORT ONLY, not implemented)
|
||||
|
||||
A correct permanent fix MUST satisfy ALL of:
|
||||
- Zero unopened packs must **NOT** grant the user a free pack.
|
||||
- No synthetic **openable** reward may be created (any placeholder must be rejected by
|
||||
`store_buy`/`open_pack`/`consume_unopened_pack`).
|
||||
- Client must **not crash** (a resolvable `mypacks` group must exist, OR the client
|
||||
must be kept from resolving `mypacks` when empty).
|
||||
- Client must **not** show "The pack you've selected is currently not available".
|
||||
- Normal Bronze/Gold/Special store categories must still work unchanged.
|
||||
- When a genuine unopened pack exists, My Packs must continue to work (Exp A).
|
||||
- Profile/economy semantics must remain correct (no coins/nextItemId/inventory drift).
|
||||
|
||||
Nothing implemented. The evidence favours investigating an **economy-safe active
|
||||
placeholder (C′)** and/or the **count-gated My-Packs default (F)**; it explicitly does
|
||||
NOT support "grant pack 70 whenever My Packs is empty".
|
||||
|
||||
---
|
||||
|
||||
## UPDATE after Experiment C′ (2026-08-13) — active non-openable placeholder tested
|
||||
|
||||
Executed C′: sentinel 65534 `state "inactive"→"active"` only; `unopenedPackIds==[]`;
|
||||
65534 kept out of `PACK_CATALOG`. Full record in `STORE_TILE_6C.md` §16. Capture:
|
||||
`store_purchasegroup_capture_active_placeholder_2026-08-12.json` (C′-vs-baseline JSON
|
||||
diff = only `65534.state`).
|
||||
|
||||
**Resolves §7 (availability predicate), partially:** `state` **DOES participate**
|
||||
(OBSERVED). `state:"active"` removed the "pack not available" dialog while the group's
|
||||
existence still prevented the crash. So the earlier §7 "deciding field UNKNOWN" is
|
||||
updated: **`state` (inactive vs active) is (at least) a deciding field** for the
|
||||
dialog. `unopened` was NOT varied and remains untested. The full predicate may still
|
||||
involve other fields, but `state` alone flips dialog→no-dialog.
|
||||
|
||||
**Updated candidate table verdict:**
|
||||
- **C′ (active placeholder, non-openable): SUPPORTED with UX caveats — best option so
|
||||
far, but NOT adopted.** No crash, no dialog, store usable, and **no automatic
|
||||
transaction/open for 65534** (only a routine boot `TRANSACTIONCANCEL` no-op).
|
||||
Caveats (OBSERVED): (1) the placeholder renders as a **visible empty pack tile**
|
||||
("0 items, 0 bronze, 0 rares", no cover) that a user could try to open (server-safe:
|
||||
opening 65534 → no-op `{}`/stale `last_pack`, no value — §16.1); (2) **navigation
|
||||
gate**: from the Store "Browse Packs" entry the Bronze/Gold/Special categories are
|
||||
not reachable until "My Packs" is opened first (not present with a genuine owned
|
||||
pack, Exp A).
|
||||
- A (real active pack 70): works cleanly but grants a real openable pack → economy
|
||||
risk; rejected as the permanent fix.
|
||||
- Candidate **F (count-gated My-Packs default)** gains weight: C′'s visible-empty-tile
|
||||
and Browse-Packs navigation gate suggest the client is being pushed to resolve/enter
|
||||
My Packs when it should not with zero packs. If a server-settable count (e.g.
|
||||
`userInfo.unopenedPacks` / `ut/v2/store` quantities) suppresses the My-Packs
|
||||
default/tile, that could remove both the crash risk and the empty-tile artifact
|
||||
without any placeholder. UNTESTED.
|
||||
|
||||
**Permanent fix: still NOT established.** Even though C′ is the first
|
||||
crash-free/dialog-free representation, the empty-tile UX + navigation gate + the
|
||||
untested "explicit placeholder selection" behavior bar adoption. Required next steps
|
||||
(design/authorize separately): (a) controlled test of explicitly focusing/opening the
|
||||
active placeholder; (b) investigate candidate F (count-gated My-Packs) to avoid a fake
|
||||
tile entirely. Do NOT adopt `state:"active"` or "grant pack 70" as the fix on current
|
||||
evidence.
|
||||
|
||||
---
|
||||
|
||||
# Candidate F — Count-Gated My Packs Navigation (READ-ONLY investigation, 2026-08-13)
|
||||
|
||||
Question: can the server make FIFA decide **not** to resolve/default into My Packs
|
||||
when the account owns zero unopened packs (avoiding any placeholder)?
|
||||
|
||||
## 1. Server-sent unopened-pack signals (inventory, OBSERVED code)
|
||||
| field / endpoint | source | value source | when sent | client consumer | conf |
|
||||
|---|---|---|---|---|---|
|
||||
| `userInfo.unopenedPacks.recoveredPacks` (via `userMassInfo`) | `utas_server.py:409-414` | `len(unopenedPackIds)` | boot massinfo; only if count>0 **or** `_UI∈{packs,full}` (default `_UI=roster` → omitted at 0) | hub unopened-pack model / My Packs badge (`FUN…vtbl[0x4e0]`, pack-opening RE) | OBSERVED (code) |
|
||||
| `/user/credits` `.unopenedPacks.recoveredPacks` | `utas_server.py:3542-3545` | `len(unopenedPackIds)` | on credits fetch; **only if count>0** | My Packs badge / CentralUnclaimedPack hub tile | OBSERVED (code+capture) |
|
||||
| `/hub` body | `utas_server.py:1449-1453` | — | hub load | — (**no pack count present**) | OBSERVED |
|
||||
| profile `unopenedPackIds` | `fut_store.py` | account state | internal | not wire-visible directly | OBSERVED |
|
||||
`pileSize`/`store quantities` (`ut/v2/store` `FutStorePackQuantities`) exist as an
|
||||
eligibility gate (`ENDPOINT_MAP.md:60`) but were **never requested** in any capture
|
||||
(8h logs); they carry a store-open `result`, not a My-Packs count.
|
||||
|
||||
## 2. Pre-store request sequence (OBSERVED, captures)
|
||||
Boot → `accountinfo → /ut/auth → settings → phishing → match/reset → userMassInfo →
|
||||
PUT store/transaction/0 (TRANSACTIONCANCEL→{}) → /hub → clientdata → /user/credits →
|
||||
GET /store/purchasegroup/all`. The only pack-count-bearing responses **before**
|
||||
`purchasegroup` are `userMassInfo`(userInfo) and `/user/credits`.
|
||||
|
||||
## 3. Baseline([]) vs Experiment A([70]) pre-store diff (OBSERVED, captures)
|
||||
The single profile change `[] → [70]` altered exactly one pre-store wire signal:
|
||||
- `/user/credits`: **`[]` → no `unopenedPacks` member** (C′ capture, all 3 fetches:
|
||||
`{"credits":…,"currencies":[…]}`); **`[70]` → `"unopenedPacks":{"preOrderPacks":0,
|
||||
"recoveredPacks":1}`** (A capture line 25). OBSERVED.
|
||||
- `userInfo.unopenedPacks`: same pattern (present at `[70]`, omitted at `[]` with
|
||||
`_UI=roster`). INFERRED from code; A-capture credits corroborates.
|
||||
- **No other pre-store field changed.**
|
||||
|
||||
Candidate signal:
|
||||
```
|
||||
Candidate: unopenedPacks.recoveredPacks (count)
|
||||
Endpoint: /user/credits and userMassInfo(userInfo)
|
||||
Baseline([]) value: ABSENT (i.e. zero)
|
||||
Experiment A([70]) value: {preOrderPacks:0, recoveredPacks:1}
|
||||
Source: utas_server.py:3542-3545 / :409-414 (= len(unopenedPackIds))
|
||||
Client-visible before purchasegroup?: YES
|
||||
Confidence: OBSERVED that it differs; its CONTROL over My-Packs nav = see §9
|
||||
```
|
||||
**Key point: this count is already CORRECT** — it reports zero (absent) when the
|
||||
account is empty. OpenFUT is **not** misreporting a nonzero pack count.
|
||||
|
||||
## 4. Navigation / client call path (OBSERVED, decompiled RE)
|
||||
`FUN_18007dab0 → FUN_1800147f0(model, screen+0x290, …)`. `screen+0x290==0` lists
|
||||
group tiles; else `FUN_180014420` exact-matches the group ordinal (NULL on miss →
|
||||
`[NULL+0x48]` crash). `screen+0x290` is written in exactly two CardsDLL sites: the
|
||||
screen ctor `FUN_18007d1a0` writes `0`, and **`FUN_18007e7f0` case `0x7551` copies
|
||||
the Flash movie message field `CATEGORY_ID` verbatim** into it
|
||||
(store-subsystem :172-175, :1055-1056). So the resolved category is chosen by the
|
||||
**Scaleform movie**, not by any server response field.
|
||||
|
||||
## 5. `GOTO_STORE_MYPACK` analysis (OBSERVED, RE)
|
||||
`GOTO_STORE_MYPACK` is the **destination of the hub `CentralUnclaimedPack` tile**
|
||||
(tile type 0x1c); "Nothing in the chain issues a request, and no request could
|
||||
exist" (pack-opening :888-890). Whether that HUB tile appears is gated by the
|
||||
unopened-pack count in the hub model (`model+0x20950`) — i.e. the count DOES control
|
||||
the *hub unclaimed-pack tile*, but the operator reached the store via **Browse Packs**
|
||||
/ the store screen, whose category resolution is the movie-driven `CATEGORY_ID` path
|
||||
(§4), not `GOTO_STORE_MYPACK`. `GOTO_STORE_MYPACK` is a UI navigation command,
|
||||
**not** a server-state-gated store-category selector.
|
||||
|
||||
## 6. Candidate count/flag fields — verdict per field
|
||||
- `unopenedPacks.recoveredPacks`: correct at 0 when empty; controls the hub badge /
|
||||
CentralUnclaimedPack tile, **not** the store's category resolver. Not a viable gate
|
||||
for the store My-Packs entry.
|
||||
- No other server field feeds `screen+0x290` (RE §4: only ctor-0 and movie
|
||||
`CATEGORY_ID`).
|
||||
|
||||
## 7. EA-capture evidence
|
||||
No EA-origin `purchasegroup`/`credits` capture for a zero-unopened-packs account
|
||||
exists in the repo (all captures are OpenFUT-generated). EA count semantics for empty
|
||||
My Packs remain **UNKNOWN**.
|
||||
|
||||
## 8. Where My Packs selection occurs (OBSERVED)
|
||||
**Before** `purchasegroup` parsing decides content, the **Scaleform movie** decides
|
||||
which category to resolve and writes it to `screen+0x290` (§4). The server's role is
|
||||
limited to which groups EXIST in `purchase[]`. Therefore the sentinel is compensating
|
||||
for a **movie-side** decision to resolve My Packs; it is not fixing an incorrect
|
||||
server count (the count is already correct).
|
||||
|
||||
## 9. Candidate F verdict — **F3 (CONTRADICTED)** (with an F4 residue)
|
||||
My Packs selection is **not** controlled by server-sent unopened-pack state:
|
||||
- OBSERVED: the server count is correctly zero/absent when empty, yet the store still
|
||||
resolved My Packs (baseline dialog, Exp-B crash). A correct zero signal did not stop
|
||||
it.
|
||||
- OBSERVED (RE): `screen+0x290` (the resolved category) comes from the movie's
|
||||
`CATEGORY_ID`, with no server-field input; default is 0.
|
||||
Residue (F4): the movie's internal logic for *why* it asks for My Packs on store open
|
||||
is in packed Scaleform and is not statically readable — but no server lever into it
|
||||
has been found. **Conclusion: there is no server-controlled count/flag that makes FIFA
|
||||
skip resolving My Packs; the server can only ensure the `mypacks` group exists.** The
|
||||
"clean count-gated fix" is therefore **not achievable server-side**.
|
||||
|
||||
## 10. Proposed next experiment
|
||||
Because F is contradicted, a count experiment is NOT recommended (the count is already
|
||||
correct and does not gate the store). No single-variable server signal will make FIFA
|
||||
enter Browse Packs instead of My Packs. The realistic next step is the previously
|
||||
deferred **explicit active-placeholder selection test**: with the C′ active
|
||||
non-openable placeholder in place (sentinel 65534 at baseline otherwise), have the
|
||||
operator explicitly focus/open the empty My-Packs tile and observe (server-safe per
|
||||
§16.1 — opening 65534 is a no-op — but UX/navigation behavior unknown). That
|
||||
characterizes the best available server-side option (C′) before any adoption.
|
||||
- Would it change profile state? No (`unopenedPackIds=[]`).
|
||||
- Would it change purchasegroup/sentinel behavior? Only `state:"active"` (as C′),
|
||||
reverted after.
|
||||
- Code change? Yes (same one-line C′ patch). Restart? Yes. (Not authorized here.)
|
||||
If explicit selection proves unsafe/ugly, the remaining options are all **client-side
|
||||
/ out-of-scope** (the decision is in the Scaleform movie), or accepting C′ with its
|
||||
documented UX artifacts.
|
||||
|
||||
**Candidate F does NOT provide the hoped-for clean fix. The active non-openable
|
||||
placeholder (C′) remains the best server-side representation; its empty-tile and
|
||||
Browse-Packs navigation artifacts are movie-driven and not server-fixable.**
|
||||
|
||||
---
|
||||
|
||||
# Explicit Active-Placeholder Selection Test — FINAL backend-side result (2026-08-13)
|
||||
|
||||
With the C′ active placeholder in place (`unopenedPackIds=[]`, 65534 active/mypacks/
|
||||
∉PACK_CATALOG), the operator explicitly opened the empty My-Packs tile once. Full
|
||||
record in `STORE_TILE_6C.md` §17.
|
||||
- **Outcome: S1 — pure client-side rejection.** Dialog **"This pack is no longer
|
||||
available"** → back to My Packs → Hub; **no crash**, navigation stays usable.
|
||||
- **No server request** on selection (no `/store/transaction`, no `/purchased/items`,
|
||||
no 65534 reference); the verdict is client-side. (OBSERVED)
|
||||
- **Zero economy/profile mutation:** coins/items/nextItemId/`unopenedPackIds`/
|
||||
`last_pack` all unchanged; profile byte-identical (`39bb3e83…`); 65534 not
|
||||
persisted. (OBSERVED)
|
||||
|
||||
**Active-placeholder verdict: MARGINALLY ACCEPTABLE** — crash-safe + economy-safe +
|
||||
navigable, but with user-visible defects (empty fake tile; "no longer available" on
|
||||
explicit click; Browse-Packs nav gate). It is a *strict improvement* over the current
|
||||
inactive-sentinel baseline (which errors on store OPEN and bounces to Hub).
|
||||
|
||||
**Backend-side question is now fully answered.** The complete zero-unopened-packs
|
||||
ladder:
|
||||
```
|
||||
no mypacks group -> CardsDLL null-deref CRASH (unsafe)
|
||||
inactive placeholder -> "pack not available" on store open -> Hub (baseline)
|
||||
active placeholder -> store loads; empty tile; "no longer available" only on
|
||||
explicit click; recoverable; economy-safe (best backend option)
|
||||
real active owned pack -> fully correct UI (but grants a real openable pack — economy risk)
|
||||
```
|
||||
|
||||
**Permanent-fix recommendation: P2.** The active non-openable placeholder is the best
|
||||
*safe* backend-only option, but a fully *clean* zero-pack experience is **not**
|
||||
achievable server-side (Candidate F CONTRADICTED — the My-Packs resolution is
|
||||
Scaleform/movie-driven). Recommend: adopt the active placeholder as an optional
|
||||
backend compatibility mode (safe, strictly better than baseline) AND pursue a
|
||||
client-side fix (hide the fake tile / stop the forced My-Packs resolution) for the
|
||||
fully clean result. **Not implemented.** Do NOT grant a real pack.
|
||||
|
||||
---
|
||||
|
||||
## Client resolver-guard experiment (2026-08-13) — RESULT F3 (crash, confounded)
|
||||
|
||||
A client-side `autopatch.py` memory guard (CardsDLL `0x180014858` `JNZ`→`JG`, routing
|
||||
category `<0` to list-all/Browse) was tested against the exact no-sentinel server condition
|
||||
(sentinel 65534 suppressed; `GET /store/purchasegroup` ids `[1,5,6,7]`, no mypacks group).
|
||||
The client **crashed at the identical resolver site `0x180014882`** (`[NULL+0x48]`), because
|
||||
it presented a **positive** My-Packs ordinal (crash is in the `>0` resolve branch), not the
|
||||
`-1` the guard diverts. **Confound:** FIFA was not relaunched after the backend flip, so it
|
||||
reused stale (sentinel-present) tab state. So the negative-only guard is **insufficient for a
|
||||
positive stale/invalid ordinal**, and the fresh-client case is **not yet decided** (needs a
|
||||
clean re-test: fresh launch with backend already no-sentinel). Backend P2 sentinel was
|
||||
restored immediately (mandatory rollback). Full record + candidate stronger guard:
|
||||
`docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md` PART III.
|
||||
|
||||
## Fresh-process no-sentinel retest (2026-08-13) — RESULT R1 (SUCCESS)
|
||||
|
||||
Re-ran the above cleanly: backend entered no-sentinel mode **while FIFA was closed**, then a
|
||||
**fresh** FIFA (pid 553220, new autopatch 552999, guard `85 ff 7f 0f` enforced) launched and
|
||||
opened the Store. The genuine no-sentinel `/store/purchasegroup` (ids `[1,5,6,7]`, no 65534/
|
||||
mypacks) is **byte-identical** to the F3 capture, so the only changed variable is client
|
||||
process lifetime. Outcome: **no crash, no dialog, Store opens on Browse Packs, packs
|
||||
navigable** (cosmetics only: no tabs / no cover art / "0 items" — pre-existing). A fresh
|
||||
client publishes category `-1` for the absent group, which `JNZ→JG` routes to Browse/list-all
|
||||
with no NULL deref. **This confirms F3 was stale-positive-ordinal contamination, and proves
|
||||
Strategy A (resolver guard) on the tested build.** Backend P2 sentinel restored immediately
|
||||
(`f416e71e…`, `state=active`) and remains production default. Full record:
|
||||
`docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md` PART IV.
|
||||
@@ -1,924 +0,0 @@
|
||||
# Store Tile Investigation (bug 6c)
|
||||
|
||||
Status: **ROOT CAUSE ESTABLISHED — P2 compatibility workaround IMPLEMENTED**
|
||||
(2026-08-13). Full investigation complete (§1-§17); backend fix landed in
|
||||
`fifa17-recon/tools/utas_server.py` `store_catalog` with regression tests in
|
||||
`fifa17-recon/tools/test_empty_mypacks.py`. The store-tiles flags are unchanged
|
||||
(`FUT_STORE_DISPLAYGROUP=ON`, `FUT_STORE_GROUPID=OFF`) and the profile is unchanged.
|
||||
|
||||
## RESOLUTION (2026-08-13)
|
||||
|
||||
**ROOT CAUSE (bug 6c):** FIFA 17's Store/Scaleform path RESOLVES the `mypacks`
|
||||
category even when the account owns zero unopened packs (the category is chosen
|
||||
client-side from the movie's `CATEGORY_ID` → `screen+0x290`; no server field gates
|
||||
it — Candidate F CONTRADICTED). CardsDLL `FUN_1800147f0` then dereferences the
|
||||
resolved group with NO null guard, so an absent `mypacks` group crashes the client
|
||||
(`CardsDLL_Win64_retail.dll+0x14882`, `[NULL+0x48]` — minidump-confirmed, §8).
|
||||
|
||||
**BACKEND RESULT / DECISION — P2 (compatibility workaround):** emit a synthetic,
|
||||
**active**, non-openable `mypacks` placeholder (id 65534, absent from `PACK_CATALOG`)
|
||||
only when `unopenedPackIds == []`. This is crash-safe AND economy-safe (explicit
|
||||
selection is rejected client-side with "This pack is no longer available", sends no
|
||||
backend request, and mutates nothing — §17). It is a FIFA-17 client-compatibility
|
||||
shim, **NOT** an EA-authentic empty-My-Packs representation, and is confined to the
|
||||
FIFA-17 adapter/backend layer (NOT OpenFUT Core).
|
||||
|
||||
**KNOWN UX LIMITATIONS (unfixable server-side):** a fake empty "0 items" tile; an
|
||||
explicit-selection dialog "This pack is no longer available"; and a Browse-Packs →
|
||||
My-Packs navigation quirk. These are Scaleform/movie-driven.
|
||||
|
||||
**CLEAN CLIENT FIX:** still unresolved; belongs to client-side work —
|
||||
`docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md`.
|
||||
|
||||
Evidence labels: **OBSERVED** (running code / `docker inspect` / live log / minidump /
|
||||
table), **INFERRED**, **HYPOTHESIS**, **UNKNOWN**.
|
||||
|
||||
### Hypotheses
|
||||
|
||||
- **H1 (original — subtype/definition):** *Store tiles render "unknown" because the
|
||||
server emits definitions whose type/subtype the client cannot map (prime suspects
|
||||
5004xxx misc {231,232,233,236}, 8010xxx league logos, FCC↔wire subtype gaps).*
|
||||
**Verdict: CONTRADICTED by static store-handler evidence.** The `/store/purchasegroup`
|
||||
handler emits PACK definitions only — no `cardsubtypeid`/`carddbid`/`cardassetid`
|
||||
anywhere in the response (§2). Those subtype families belong to the separate
|
||||
**club-item / consumable / equippable** paths (`fut_clubitems.py`, `fut_consumables`,
|
||||
`/club?type=`), which are OUT OF SCOPE for this store-tile task. History preserved
|
||||
in §3.3 and §8; not investigated further here.
|
||||
- **H2 (revised — displayGroup token):** **HYPOTHESIS (under runtime test).** *The
|
||||
"unknown" FUT Store tile is caused by one or more pack entries whose
|
||||
`displayGroup.value` token is not one of the six categories FIFA 17 can render:*
|
||||
`mypacks, points, bronze, silver, gold, special`. Tested against a real capture in
|
||||
§4-§8.
|
||||
|
||||
---
|
||||
## Runtime capture plan (Phase 2) — OBSERVED
|
||||
|
||||
- **Backend container:** `openfut-fut-backend` (logs on stdout via `log()`,
|
||||
`utas_server.py:59-62`; each request logs `"<VERB> <path>"` at `:3732`, and the
|
||||
response line `" -> <code> <body[:200]>"` at `:3754` — **response body truncated
|
||||
to 200 bytes**, so the full JSON body is NOT in the log).
|
||||
- **Endpoint / path matcher:** `GET /ut/game/fifa17/store/purchasegroup/all?ppInfo=true`
|
||||
(OBSERVED historically in the log; route regex `/store/purchasegroup`,
|
||||
`utas_server.py:1215`).
|
||||
- **Capture marker (UTC, set immediately before the manual test):**
|
||||
`2026-08-12T23:54:01Z` (saved to `/tmp/store_capture_marker.txt`; 0 log lines
|
||||
after it at set-time → clean boundary).
|
||||
- **Log command to isolate the manual action:**
|
||||
`docker logs --since 2026-08-12T23:54:01Z --timestamps openfut-fut-backend`
|
||||
then locate the first `GET .../store/purchasegroup` line after the marker plus its
|
||||
following headers and `-> 200 {"purchase"...` line.
|
||||
- **Full-body recovery (because the log truncates at 200 bytes):** the response is a
|
||||
deterministic pure function — `store_catalog()` (`:3408`) over static `PACK_CATALOG`
|
||||
(`fut_store.py:820`) + live `STORE.unopened_packs()` (from `/state` profile) under
|
||||
fixed flags (`STORE_DISPLAYGROUP=ON`, `STORE_GROUPID=OFF`, `FUT_PRICE_PROBE=OFF`).
|
||||
Plan: reconstruct the exact body from the running `/app` code + live `/state`
|
||||
profile, then **verify** its `json.dumps(...)[:200]` byte-for-byte equals the genuine
|
||||
logged 200-byte prefix. Match ⇒ the reconstruction IS the sent body. No request is
|
||||
synthesized, replayed, or curl'd; the genuine log line is the ground-truth anchor.
|
||||
|
||||
|
||||
## 1. Method
|
||||
|
||||
### Environment (OBSERVED)
|
||||
- Running on `10.10.0.120` (dev-lxc). Client is `10.10.0.105`.
|
||||
- Backend under test: Docker container `openfut-fut-backend`
|
||||
(image `openfut-fut-backend:dev`), `Up 7 hours`, entrypoint `/app/entrypoint.sh`.
|
||||
- `docker inspect openfut-fut-backend`: only mount is
|
||||
`/home/alex/OpenFUT/fifa17-recon/docker/state -> /state (rw)`; container ENV
|
||||
contains **no `FUT_STORE_*`** vars.
|
||||
- `entrypoint.sh` launches `python3 -u utas_server.py` from `/app/tools` with extra
|
||||
env `FUT_TRADING=1 FUT_PILESIZES=1 FUT_TRADEABLE=1 FUT_DISCARD_TABLE=1
|
||||
FUT_DISCARD_SEND=1` — again **no `FUT_STORE_*`**.
|
||||
- The running store code is `/app/tools/utas_server.py`. Extracted read-only via
|
||||
`docker cp openfut-fut-backend:/app/tools /tmp/app-tools` and confirmed
|
||||
**byte-identical** (`sha256`) to the repo copy
|
||||
`fifa17-recon/tools/utas_server.py` (both 3765 lines) and
|
||||
`fifa17-recon/tools/fut_clubitems.py`. All line references below are to the repo
|
||||
paths and equal the running code.
|
||||
|
||||
### Commands (exact)
|
||||
```
|
||||
docker ps --format '{{.Names}}\t{{.Image}}\t{{.Command}}\t{{.Status}}'
|
||||
docker inspect openfut-fut-backend --format '...CMD/ENTRYPOINT/MOUNTS/ENV...'
|
||||
docker exec openfut-fut-backend cat /app/entrypoint.sh
|
||||
docker cp openfut-fut-backend:/app/tools /tmp/app-tools
|
||||
docker cp openfut-fut-backend:/app/data /tmp/app-data
|
||||
diff /tmp/app-tools/utas_server.py fifa17-recon/tools/utas_server.py # identical
|
||||
diff /tmp/app-tools/fut_clubitems.py fifa17-recon/tools/fut_clubitems.py # identical
|
||||
```
|
||||
|
||||
### Manual test sequence
|
||||
NOT executed. The manual FIFA-client store capture (2D/2E) was never reached
|
||||
because the investigation blocked at 2C before any flag could be enabled. No
|
||||
request was synthesized, replayed, or simulated.
|
||||
|
||||
---
|
||||
|
||||
## 2. Store handler code path (2A) — OBSERVED
|
||||
|
||||
Request → response for the store tile screen:
|
||||
|
||||
1. **Endpoint.** `GET ut/<sku>/store/purchasegroup/...` — `FutStoreGetPackTypes`
|
||||
(client deser root `0x1801234e0`). (`utas_server.py:3408-3409` docstring.)
|
||||
- Route table entry: `(re.compile(r"/store/purchasegroup"), lambda m,h:
|
||||
store_catalog(h))` at `utas_server.py:1215`.
|
||||
- Sibling store routes: `/store/transaction -> store_buy(h)` (`:1216`, the BUY);
|
||||
bare `/store(\?|$) -> (200,{"result":"SUCCESS"})` eligibility gate (`:1221`).
|
||||
2. **Handler.** `store_catalog(h)` (`utas_server.py:3408-3447`).
|
||||
- Iterates `PACK_CATALOG` (non-`ownedOnly` packs) → `_pack_body(p, idx)`.
|
||||
- Appends owned unopened packs from `visible_unopened_packs()` →
|
||||
`STORE.unopened_packs()` + `_OPENED_PACK_GRACE` (`:51-52`, `:3423-3427`).
|
||||
- If no owned packs, appends one inactive `mypacks` sentinel (id 65534) so
|
||||
`GOTO_STORE_MYPACK` resolves (`:3428-3446`).
|
||||
- Returns `200, {"purchase": [<pack bodies>], "timestamp": 1596326400}`
|
||||
(`:3447`).
|
||||
3. **Definition construction.** `_pack_body(p, idx, owned=False)`
|
||||
(`utas_server.py:3268-3405`). Emitted keys (OBSERVED, `:3293-3328`):
|
||||
`assetId`(=`p["id"]`), `id`(=`p["id"]`), `packType`, `description`(=`p["name"]`),
|
||||
`state`, `saleType`, `limitType`, `quantity`, `purchaseLimit`, `purchaseCount`,
|
||||
`isPremium`, `sortPriority`, `currencies`, `extPrice`, `packContentInfo`
|
||||
(`bronze/silver/gold/rare/itemQuantity`), `unopened`, and one of:
|
||||
- owned pack → `displayGroup = {"value":"mypacks","priority":idx}` (`:3336`);
|
||||
- else if `STORE_DISPLAYGROUP` → `displayGroup = {"value": category}` where
|
||||
`category ∈ {special, gold, silver, bronze}` chosen from
|
||||
`p["specialChance"]`/`p["gold"]` (`:3337`,`:3373-3379`), and if `STORE_GROUPID`
|
||||
also `displayGroupAssetId = p["id"]` (`:3403-3404`).
|
||||
4. **Data source(s).**
|
||||
- `PACK_CATALOG` — a hardcoded list of 3 pack dicts
|
||||
(`{id,name,price,count,gold,tiers,specialChance}`) at `fut_store.py:820-841`.
|
||||
There is NO card table read in this path.
|
||||
- `STORE = Store()` (`fut_store.py:843`), backed by the profile JSON
|
||||
(`unopened_packs()` reads `unopenedPackIds`, `fut_store.py:619-621`).
|
||||
5. **Serialization → HTTP.** The dict is JSON-encoded by the server's response
|
||||
writer and returned as the HTTP body.
|
||||
|
||||
### Fields 5 (`cardsubtypeid`/`carddbid`/`cardassetid`) — OBSERVED
|
||||
**None of `cardsubtypeid`, `carddbid`, or `cardassetid` appear anywhere in the
|
||||
store-tile (`purchasegroup`) response.** `_pack_body` (`:3293-3405`) emits only the
|
||||
pack keys listed above; `assetId`/`id` are the PACK id `p["id"]` (e.g. 1, 5), not a
|
||||
card asset id. The store catalog emits PACKS, never card definitions. (Grep of
|
||||
`_pack_body` and `store_catalog` for those three field names returns zero hits.)
|
||||
|
||||
### Families the store handler can emit
|
||||
Only **packs** (`PACK_CATALOG`: "Bronze Pack" id 1, "Gold Pack" id 5, and the third
|
||||
catalog entry) plus owned reward packs and the `mypacks` sentinel. It cannot emit
|
||||
any card family (players, staff, consumables, club items).
|
||||
|
||||
### Filtering / transformation
|
||||
- `normal = [p for p in PACK_CATALOG if not p.get("ownedOnly")]` (`:3421`).
|
||||
- `_pack_body` maps a pack to a category token via `specialChance>=1.0 -> special`,
|
||||
else `gold -> gold`, `p.get("silver") -> silver`, else `bronze` (`:3373-3379`).
|
||||
- The tile caption/category is `displayGroup.value`; `description` carries the
|
||||
per-pack title.
|
||||
|
||||
---
|
||||
|
||||
## 3. Wire subtype coverage (2B)
|
||||
|
||||
### 3.1 Store path
|
||||
The store-tile path emits **no `cardsubtypeid`** at all (see §2). So for the store
|
||||
tile, "is `cardsubtypeid` the raw FCC subtype, the wire category, transformed, or
|
||||
something else?" → **not present** (N/A). The store tile is selected by
|
||||
`displayGroup.value` (a category-token STRING), not by any card subtype.
|
||||
|
||||
Per `_pack_body:3367-3372` (citing `FUN_180014580`/`FUN_180014df0`): FIFA 17's
|
||||
StoreFront resolves exactly **six hard-coded category tokens** —
|
||||
`mypacks, points, bronze, silver, gold, special`. Any other `displayGroup.value`
|
||||
(e.g. a raw pack title) creates an "unsupported pseudo-category". The documented
|
||||
cause of "unknown" store tiles is therefore a **`displayGroup` category-token**
|
||||
issue on packs (absent group, or a non-canonical token), NOT a card type/subtype.
|
||||
|
||||
### 3.2 Club-item path (the only place wire subtypes live) — `fut_clubitems.py`
|
||||
Club items are served on the **`/club?type=` route** (`utas_server.py:2250`,
|
||||
`handle_club`), gated by `FUT_CLUBITEMS`, NOT by the store route. Current `FAMILIES`
|
||||
(`fut_clubitems.py:61-67`), format `(family, table, art id, stat id, stat name,
|
||||
UNVERIFIED cardsubtypeid)`:
|
||||
|
||||
| wire subtype | mapped family | table | art id | source | confidence |
|
||||
|---|---|---|---|---|---|
|
||||
| 9 | kits | fcc_kitcards.json | 35 | `fut_clubitems.py:65` | HYPOTHESIS (marked "UNVERIFIED", `:49`,`:30-32`) |
|
||||
| 10 | stadia | fcc_stadium.json | 36 | `fut_clubitems.py:63` | HYPOTHESIS ("UNVERIFIED") |
|
||||
| 11 | badges | fcc_badgecards.json| 39 | `fut_clubitems.py:64` | HYPOTHESIS ("UNVERIFIED") |
|
||||
| 30 | balls | fcc_balls.json | 37 | `fut_clubitems.py:62` | HYPOTHESIS ("UNVERIFIED") |
|
||||
| 31 | leaguelogos | fcc_leaguelogos.json| 40| `fut_clubitems.py:66` | HYPOTHESIS ("UNVERIFIED") |
|
||||
|
||||
- The prior recorded mapping (9=kits,10=stadia,11=badges,30=balls,31=logos) is
|
||||
**confirmed present in current code** — but the code itself marks every one
|
||||
"UNVERIFIED cardsubtypeid" and states the binary assigns family↔subtype **nowhere**
|
||||
in the 149 dumped tables; cardtype-9 admits the set `{30,31,145,146,147,148,149,150}`
|
||||
(`fut_clubitems.py:26-28`, `:73`). So these are server-chosen HYPOTHESIS values.
|
||||
- In the club-item wire item (`_item:88-119`), `cardsubtypeid` (`:97`) is a
|
||||
**server-assigned wire-category constant** (9/10/11/30/31), NOT the raw FCC
|
||||
subtype: the club-item fcc tables carry **no `cardsubtype` column at all** (Task 1:
|
||||
badges/stadium/kit/logos/balls have empty subtype sets). `resourceId`/`assetId`
|
||||
= `carddbid`, and `cardassetid` = the family ART id (`:94-96`). So for club items:
|
||||
**`cardsubtypeid` = wire category (server constant), `carddbid`/`cardassetid` =
|
||||
real fcc columns.**
|
||||
- By contrast, consumables (`fut_store._item` / `fut_consumables`) DO carry the raw
|
||||
FCC subtype (51..341) as `cardsubtypeid`.
|
||||
|
||||
### 3.3 Task-1 families vs wire-subtype coverage
|
||||
Families that the **store handler** can map to a wire subtype: **none** — the store
|
||||
handler emits packs, which have no card subtype (by design).
|
||||
|
||||
Families for which a **club-item** wire subtype exists (HYPOTHESIS-grade):
|
||||
kits(9), stadia(10), badges(11), balls(30), leaguelogos(31).
|
||||
|
||||
Task-1 card families with **no wire subtype mapping anywhere in the server**:
|
||||
- **5001xxx contracts, 5002xxx fitness/healing, 5003xxx training** — served as
|
||||
consumables carrying their raw FCC subtype; these are consumable overlays, not
|
||||
store tiles, and not part of any store/club-item wire-category map.
|
||||
- **5004xxx misc {231,232,233,236}** — **no wire subtype mapping found** in
|
||||
`fut_clubitems.py` or the store path. (Grep: no `misc` family, no {231,232,233,236}
|
||||
wire assignment.) They exist only as raw FCC subtypes in consumable data.
|
||||
- **8010xxx league logos/stickers** — mapped in the **club-item** path as wire
|
||||
subtype **31** (`fut_clubitems.py:66`), HYPOTHESIS-grade. `fcc_leaguelogostickers`
|
||||
(39 rows) is NOT wired in `FAMILIES` (only `fcc_leaguelogos`, 44 rows).
|
||||
- **6000xxx badges, 6200xxx stadiums, 6300/6400xxx kits, 8120xxx balls** — mapped in
|
||||
the club-item path (11/10/9/30), HYPOTHESIS-grade.
|
||||
- **staff (managers/coaches, subtypes 4-8)** — served by `fut_staff` on `/club?type=
|
||||
manager`, not a store tile.
|
||||
|
||||
Note none of these belong to the **store-tile** (`purchasegroup`) response.
|
||||
|
||||
---
|
||||
|
||||
## 2C flag investigation — BLOCKED
|
||||
|
||||
### Store-tiles flags located (OBSERVED)
|
||||
Two, both in `utas_server.py`, both module-level constants read **once at import**:
|
||||
|
||||
| flag | env var | line (read) | consumed | default | current running value |
|
||||
|---|---|---|---|---|---|
|
||||
| `STORE_DISPLAYGROUP` | `FUT_STORE_DISPLAYGROUP` | `:975` | `_pack_body:3337` | `"1"` → **ON** | **ON** (no env override) |
|
||||
| `STORE_GROUPID` | `FUT_STORE_GROUPID` | `:980` | `_pack_body:3403` | `"0"` → **OFF** | **OFF** (no env override) |
|
||||
|
||||
- `:975` `STORE_DISPLAYGROUP = os.environ.get("FUT_STORE_DISPLAYGROUP", "1") == "1"`
|
||||
- `:980` `STORE_GROUPID = os.environ.get("FUT_STORE_GROUPID", "0") == "1"`
|
||||
|
||||
**Current values (recorded before any change; nothing was changed):**
|
||||
- `FUT_STORE_DISPLAYGROUP`: unset in container env → default `"1"` →
|
||||
`STORE_DISPLAYGROUP = True` (**ON**). (INFERRED from OBSERVED env dump + OBSERVED
|
||||
code default.)
|
||||
- `FUT_STORE_GROUPID`: unset in container env → default `"0"` →
|
||||
`STORE_GROUPID = False` (**OFF**). This is the flag "expected to be OFF".
|
||||
|
||||
The related club-item flag `FUT_CLUBITEMS` (`:1647-1648`) is likewise unset →
|
||||
`CLUBITEMS = False` (club items not currently served), also a module-level import
|
||||
constant.
|
||||
|
||||
### Dynamic evaluation? NO (OBSERVED)
|
||||
- All three flags are top-level `os.environ.get(...)` assignments evaluated at
|
||||
module import (`:975`, `:980`, `:1647`); `_pack_body` reads the resulting module
|
||||
**constants** (`:3337`, `:3403`), never `os.environ` at request time.
|
||||
- Repo-wide there is **no** `importlib.reload`, no `signal`/`SIGHUP` handler, and no
|
||||
per-request environ re-read for these flags. (The only runtime-refreshable feature
|
||||
is `FUT_ID_SWEEP`, which re-reads a *file* `SWEEP_FILE`, `:1965-1966` — unrelated.)
|
||||
- `:3250` documents the intended workflow explicitly:
|
||||
`# Enable for the test with: FUT_PRICE_PROBE=1 ./openfut-fut.sh restart`.
|
||||
|
||||
### Restart conflict → STOP
|
||||
Changing either store flag requires either setting a container env var (→
|
||||
`docker` recreate = restart) or editing the module (→ re-import = restart). Both
|
||||
violate the no-restart / no-redeploy / no-recreate constraint. Therefore:
|
||||
|
||||
```
|
||||
TASK 2 BLOCKED AT 2C:
|
||||
Flag requires restart, conflicting with no-restart constraint.
|
||||
```
|
||||
|
||||
No flag was enabled. 2D/2E (manual client capture + definition analysis) NOT
|
||||
started. No client request generated, replayed, or simulated.
|
||||
|
||||
---
|
||||
|
||||
## 4. Captured request and response (Phase 3) — OBSERVED
|
||||
|
||||
Real client action (operator opened the FUT Store on `.105`, 2026-08-12). Only one
|
||||
`/store/purchasegroup` request occurred after the capture marker
|
||||
`2026-08-12T23:54:01Z`, so attribution is unambiguous (only the operator drives the
|
||||
client). Log via `docker logs --since 2026-08-12T23:54:01Z --timestamps openfut-fut-backend`.
|
||||
|
||||
- **Request:** `23:54:43 GET /ut/game/fifa17/store/purchasegroup/all?ppInfo=true`
|
||||
(Host `10.10.0.120:8099`, `User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)`,
|
||||
`X-UT-SID` present). Preceded at 23:54:43 by `GET /ut/game/fifa17/user/credits`
|
||||
→ `200 {"credits": 29876776, ...}`.
|
||||
- **Response:** `200`. The server log truncates the body to 200 bytes
|
||||
(`utas_server.py:3754`, `raw[:200]`), genuine prefix:
|
||||
`{"purchase": [{"assetId": 1, "id": 1, "packType": "BRONZE", "description": "Bronze Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount`
|
||||
- **Full body recovered** by running the exact running-container code
|
||||
(`docker exec openfut-fut-backend python3 -c "import utas_server as u; u.store_catalog(None)"`)
|
||||
over the live `/state` profile, under the live flags (confirmed in-process:
|
||||
`STORE_DISPLAYGROUP=True`, `STORE_GROUPID=False`). Its `json.dumps(...)[:200]`
|
||||
equals the genuine logged 200-byte prefix **byte-for-byte** (verified MATCH), so the
|
||||
reconstruction IS the sent body (deterministic pure function + verified prefix). No
|
||||
request was synthesized, replayed, or curl'd. Full body (2841 bytes) preserved
|
||||
verbatim at `docs/evidence/store_purchasegroup_capture_2026-08-12.json`.
|
||||
- **Operator-reported client behavior:** on opening the store, an error dialog
|
||||
appeared — *"The pack you've selected is currently not available. Please select a
|
||||
different pack or try again later."* — and clicking OK returned to the FUT hub. No
|
||||
tiles were browsable; **no "unknown" tiles were reported**. There was **no**
|
||||
`PUT /store/transaction` (no buy) and **no server error** (server answered `200`).
|
||||
|
||||
## 5. Definition analysis (Phase 4/5) — OBSERVED
|
||||
|
||||
`purchase[]` = 5 entries (`timestamp` 1596326400). No `cardsubtypeid`/`carddbid`/
|
||||
`cardassetid` in any entry (packs, not cards).
|
||||
|
||||
| idx | id | assetId | packType | description | displayGroup.value | priority | state | dgAssetId | classification |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 0 | 1 | 1 | BRONZE | Bronze Pack | `bronze` | — | active | absent | KNOWN TOKEN |
|
||||
| 1 | 5 | 5 | GOLD | Gold Pack | `gold` | — | active | absent | KNOWN TOKEN |
|
||||
| 2 | 6 | 6 | GOLD | Premium Gold | `gold` | — | active | absent | KNOWN TOKEN |
|
||||
| 3 | 7 | 7 | GOLD | Special Players Pack | `special` | — | active | absent | KNOWN TOKEN |
|
||||
| 4 | 65534 | 65534 | GOLD | "" (empty) | `mypacks` | 1 | **inactive** | absent | KNOWN TOKEN |
|
||||
|
||||
Per-pack fields (idx 0-3 identical shape): `saleType:"promo"`, `limitType:"NONE"`,
|
||||
`quantity:0`, `purchaseLimit:0`, `purchaseCount:0`, `isPremium:false`,
|
||||
`currencies:[{"name":"coins","funds":<price>,"finalFunds":<price>}]`,
|
||||
`extPrice:{finalPrice/originalPrice:{amount:<price/100>,currency:"mtx"}}`,
|
||||
`packContentInfo:{...tier quantities...}`, `unopened:false`. Prices: Bronze 400,
|
||||
Gold 5000, Premium Gold 15000, Special Players 25000. The sentinel (idx 4) drops
|
||||
`currencies`/`extPrice`, has empty description, `state:"inactive"`.
|
||||
|
||||
**Unique `displayGroup.value` set emitted: `{bronze, gold, special, mypacks}`.**
|
||||
Compared to the six client renderer tokens `{mypacks, points, bronze, silver, gold,
|
||||
special}` (`FUN_180014580`/`FUN_180014df0`, `plan-2026-08-05-store-subsystem.md:202-206`):
|
||||
**every emitted token is a KNOWN token; the set outside the renderer categories is
|
||||
EMPTY.**
|
||||
|
||||
## 6. Unmapped definitions / suspect tokens found — OBSERVED
|
||||
|
||||
None. Zero SUSPECT/UNKNOWN `displayGroup.value` tokens; zero card definitions;
|
||||
zero `cardsubtypeid`/`carddbid`/`cardassetid`. The only anomalous element is the
|
||||
**inactive, empty-description `mypacks` sentinel** (idx 4), emitted by
|
||||
`store_catalog:3428-3446` **only when the profile owns zero unopened packs**
|
||||
(OBSERVED: profile `unopenedPackIds == []`).
|
||||
|
||||
## 7. Client-side evidence (Phase 7) — existing RE only
|
||||
|
||||
No new Ghidra run. Existing decompiler evidence already bounds the answer and shows
|
||||
new static analysis would be unproductive:
|
||||
- **The parser is not the gate** (`OPENCODE_ENDPOINT_PROMPT.md:118-120`): per-pack
|
||||
epilogue `0x18013badc` pushes every parsed pack unconditionally — no drop predicate
|
||||
in the parse path. So a `200` with clean JSON cannot be rejected by the deserializer.
|
||||
- **Store-level "not available"** (`FUT_CatalogNotAvailable`, msg `0x7550`) comes from
|
||||
downstream client gates (`OPENCODE_ENDPOINT_PROMPT.md:120-131`): (1) resolution
|
||||
`GetSystemMetrics` ≤1024×768, (2) store-data-model load status `0x180013cf0`,
|
||||
(3) Blaze purchase-config flags `IS_STORE_ENABLED/IS_COIN_PURCHASABLE/...` (these
|
||||
are already served, `blaze_responder_v3b.py:708-719`).
|
||||
- **Per-pack tile-availability predicate is in the PACKED FIFA17.exe and UNREAD**
|
||||
(`plan-2026-08-05-pack-opening.md:553`): fields `state/saleType/quantity/
|
||||
purchaseLimit/purchaseCount/start/end` are parsed and copied to the tile, but the
|
||||
predicate that greys/blocks a tile "is in the packed exe and unread." Whether
|
||||
`purchaseLimit`+`purchaseCount` greys a tile is explicitly an OPEN question
|
||||
(`:644-645`). Static Ghidra on the packed exe cannot read it (decrypts only in live
|
||||
memory); resolving it requires a live-memory experiment on the running FIFA process,
|
||||
which is out of scope (must not touch FIFA).
|
||||
- The exact operator string *"The pack you've selected is currently not available"*
|
||||
is **not present** anywhere in the recon corpus (docs/tools); the documented
|
||||
store/pack error strings are `FUT_CatalogNotAvailable` and
|
||||
`CARDS_CB_ERR_PACK_NOT_IN_DIME` (a server-returnable code). UNKNOWN loc key.
|
||||
|
||||
## 8. Assessment (Phase 8) — runtime verdict
|
||||
|
||||
- **H1 (subtype/definition): CONTRADICTED.** The captured response contains no card
|
||||
definitions and no `cardsubtypeid`/`carddbid`/`cardassetid` (OBSERVED §4-§5).
|
||||
- **H2 (unknown `displayGroup.value` token): CONTRADICTED.** Every emitted token is a
|
||||
KNOWN renderer category (`bronze/gold/special/mypacks`); the unsupported-token set
|
||||
is EMPTY (OBSERVED §5). The store-tile "unknown" mechanism is NOT reproduced under
|
||||
the current config (`STORE_DISPLAYGROUP=ON`).
|
||||
|
||||
Answering the Phase-8 questions:
|
||||
1. **Unsupported `displayGroup.value` in the response?** No — all four tokens
|
||||
(`bronze/gold/special/mypacks`) are recognized. (OBSERVED)
|
||||
2. **Which pack got it?** None. (OBSERVED)
|
||||
3. **Correspond to an unknown tile in FIFA?** No unknown tile was reported; the
|
||||
observed symptom was a *"pack not available"* dialog, not an unknown tile. (OBSERVED)
|
||||
4. **Where does the backend assign the token?** `_pack_body:3373-3379` maps each pack
|
||||
to `special/gold/silver/bronze` from `specialChance`/`gold`; owned/sentinel →
|
||||
`mypacks` (`:3336`). All canonical. (OBSERVED)
|
||||
5. **Is `displayGroup.value` sufficient to explain the bug?** No. The response is
|
||||
clean; the failure is a downstream client/packed-exe gate, not a token. (INFERRED)
|
||||
6. **Is the server emitting a value FIFA demonstrably cannot understand?** No.
|
||||
(OBSERVED)
|
||||
7. **Narrowest likely fix (REPORT ONLY — not implemented):** The single anomalous,
|
||||
server-controllable element is the **inactive empty `mypacks` sentinel** emitted
|
||||
when `unopenedPackIds == []` (`store_catalog:3428-3446`). Leading HYPOTHESIS: with
|
||||
no owned packs, the store's My-Packs group contains only this inactive pack, and
|
||||
the client's (packed-exe) selection/availability path lands on it →
|
||||
*"the pack you've selected is currently not available"* → back to hub. Narrowest
|
||||
candidate fixes to TEST (each needs a controlled change, hence a future
|
||||
restart-gated experiment — do NOT implement now):
|
||||
(a) suppress the sentinel when there are no owned packs and instead let the store
|
||||
land on a real active category (bronze/gold/special); or
|
||||
(b) if My-Packs must resolve, make the sentinel non-selectable rather than an
|
||||
`inactive` pack in the group.
|
||||
Cheaper-to-eliminate CLIENT-side cause to check first (existing RE, no server
|
||||
change): FIFA display resolution must be **>1024×768** on `.105`
|
||||
(`OPENCODE_ENDPOINT_PROMPT.md:122-124`).
|
||||
|
||||
**Overall runtime verdict: CONTRADICTED** — neither H1 nor H2 reproduces; the
|
||||
"unknown store tile" hypothesis is not the live failure. The live failure is a
|
||||
distinct *pack-availability* error whose trigger is in the packed FIFA17.exe and
|
||||
cannot be pinned from the (clean) server response alone.
|
||||
|
||||
## 9. Open questions
|
||||
1. What exactly raises *"The pack you've selected is currently not available"*? The
|
||||
loc key is unknown and the predicate is in the packed exe (unread). Resolving it
|
||||
needs a controlled field/flag experiment or live-memory RE (both currently gated).
|
||||
2. Does the store, with `unopenedPackIds == []`, land on / auto-select the inactive
|
||||
`mypacks` sentinel? (HYPOTHESIS §8.7; unproven without client-side observation.)
|
||||
3. Did the store render tiles successfully in earlier sessions when the profile
|
||||
owned unopened packs (e.g. the 21:54-21:57 pack-opening burst)? If so, the
|
||||
presence/absence of owned packs (sentinel) is implicated. (UNKNOWN — earlier logs
|
||||
truncate the body; not proven.)
|
||||
4. Does `purchaseLimit:0`/`purchaseCount:0` grey a tile? Existing RE lists this as an
|
||||
OPEN question; would need a controlled experiment. (UNKNOWN)
|
||||
5. Is bug 6c ("unknown tile") a stale symptom from before `STORE_DISPLAYGROUP` became
|
||||
the default `ON`? Under the current config no unknown tile reproduces.
|
||||
|
||||
---
|
||||
|
||||
## H3 — EMPTY MY-PACKS SENTINEL (HYPOTHESIS)
|
||||
|
||||
When the profile owns zero unopened packs (`unopenedPackIds == []`), OpenFUT emits
|
||||
synthetic **inactive** pack id **65534** in the `mypacks` display group
|
||||
(`store_catalog:3428-3446`). The FIFA 17 store may treat this object as a selectable
|
||||
pack whose availability predicate fails, producing *"The pack you've selected is
|
||||
currently not available"* and returning the user to the FUT Hub before any
|
||||
`/store/transaction`. **Status: HYPOTHESIS (untested).**
|
||||
|
||||
## 10. Resolution check (Phase 1) — OBSERVED
|
||||
|
||||
Read-only inspection of `.105` (FIFA pid 529227, not touched):
|
||||
- Desktop/monitor: **2560x1440** — DRM connectors `card1-DP-2` and `card1-HDMI-A-1`
|
||||
both `connected`, native mode 2560x1440; compositor KDE `kwin_wayland` (no gamescope).
|
||||
- FIFA render config: `…/Games/umu/fifa17/pfx/drive_c/users/steamuser/Documents/FIFA 17/settings/overrideAutodetect.lua`
|
||||
→ `ResolutionWidth = 1280`, `ResolutionHeight = 720`, `FullscreenEnabled = 0` (windowed).
|
||||
- Session: Wayland (`WAYLAND_DISPLAY=wayland-0`), FIFA via XWayland (`DISPLAY=:0`,
|
||||
`XAUTHORITY=/run/pressure-vessel/Xauthority` inside the game namespace).
|
||||
|
||||
**Is FIFA rendering above 1024x768? YES (1280x720).**
|
||||
**Resolution hypothesis eliminated for this reproduction.**
|
||||
|
||||
## 11. Sentinel 65534 provenance (Phase 3)
|
||||
|
||||
1. **Basis:** **OpenFUT INVENTION** (OBSERVED). `65534` (0xFFFE) appears nowhere in
|
||||
any EA capture or recon note — repo-wide it exists only in `utas_server.py`
|
||||
(`store_catalog:3435-3446`) and this evidence set. The author's comment
|
||||
(`:3428-3434`) states it is a workaround: "Retain an inactive zero-item sentinel so
|
||||
the [`mypacks`] destination resolves… Its id is deliberately absent from
|
||||
PACK_CATALOG." It is a compatibility guess, not captured behavior.
|
||||
2. **Known-good EA capture of EMPTY My Packs:** **UNKNOWN** — none found. All recon
|
||||
My-Packs analysis is client-side RE (`FUN_1800150d0` filters
|
||||
`displayGroup.value=="mypacks"`, `plan-2026-08-05-pack-opening.md:34-36,893-895`);
|
||||
no EA server response for an empty My Packs state is on record.
|
||||
3. **Known-good response with ≥1 unopened pack:** **UNKNOWN** for EA. OpenFUT's own
|
||||
seed grants reward pack id 70 (`_new_profile` `unopenedPackIds:[70]`,
|
||||
`fut_store.py:360`), but that is an OpenFUT synthetic grant, not an EA capture.
|
||||
4. **Evidence FIFA EXPECTS a sentinel/placeholder:** **NO / UNKNOWN.** Recon shows My
|
||||
Packs is a client-side filter over the ordinary catalogue; the "empty-category
|
||||
dialog over the wrong tab" concern behind the sentinel is the author's HYPOTHESIS,
|
||||
not decompiler-confirmed. No evidence FIFA requires a placeholder object.
|
||||
5. **Evidence for the `inactive` representation:** **UNKNOWN / HYPOTHESIS.** The claim
|
||||
"state != active keeps it out of the visible row list" (`:3432`) is an unverified
|
||||
author assumption; no RE shows `state:"inactive"` hides a pack from selection. If
|
||||
FIFA does NOT hide it, the sole `mypacks` entry is a selectable inactive pack —
|
||||
exactly the H3 failure mode.
|
||||
|
||||
## 12. Empty vs non-empty My Packs behavior (Phase 2) — OBSERVED (code) / captured
|
||||
|
||||
`store_catalog(h)` (`utas_server.py:3421-3447`):
|
||||
- Always emits the 4 non-`ownedOnly` catalogue packs (ids 1,5,6,7) via `_pack_body`.
|
||||
- For each id in `visible_unopened_packs()` (= `STORE.unopened_packs()` +
|
||||
`_OPENED_PACK_GRACE`) that resolves in `PACK_CATALOG`, appends `_pack_body(owned,
|
||||
idx, owned=True)`.
|
||||
- **Only when `unopened_packs()` is empty (`if not owned_ids`)** appends the inactive
|
||||
sentinel (`:3428-3446`).
|
||||
|
||||
Sentinel 65534 vs a normal active pack (Bronze, idx 0), field-by-field (from the
|
||||
captured body):
|
||||
|
||||
| field | sentinel 65534 | Bronze Pack (active) |
|
||||
|---|---|---|
|
||||
| id | 65534 | 1 |
|
||||
| assetId | 65534 | 1 |
|
||||
| packType | GOLD | BRONZE |
|
||||
| description | "" (empty) | "Bronze Pack" |
|
||||
| state | **inactive** | active |
|
||||
| saleType | promo | promo |
|
||||
| limitType | NONE | NONE |
|
||||
| quantity | 0 | 0 |
|
||||
| purchaseLimit | 0 | 0 |
|
||||
| purchaseCount | 0 | 0 |
|
||||
| isPremium | false | false |
|
||||
| sortPriority | 1 | 1 |
|
||||
| currencies | **ABSENT** (popped) | `[{coins,400,400}]` |
|
||||
| extPrice | **ABSENT** (popped) | `{mtx 4/4}` |
|
||||
| packContentInfo | all-zero quantities | bronze 5 / item 5 |
|
||||
| unopened | false | false |
|
||||
| displayGroup.value | **mypacks** | bronze |
|
||||
| displayGroup.priority | 1 | ABSENT |
|
||||
| displayGroupAssetId | ABSENT | ABSENT |
|
||||
|
||||
**What changes when `unopenedPackIds` is non-empty (e.g. `[70]`):** the sentinel is
|
||||
NOT emitted; instead pack 70 (Reward Special Players Pack, `ownedOnly`) appears via
|
||||
the owned branch of `_pack_body` (`:3335-3336`): `displayGroup={"value":"mypacks",
|
||||
"priority":idx}`, `state:"active"` (default), `unopened:true`, `currencies`/`extPrice`
|
||||
popped. i.e. the `mypacks` group would hold a genuine **active** owned pack instead of
|
||||
the inactive sentinel.
|
||||
|
||||
## 13. Proposed controlled experiments (Phase 5) — DESIGN ONLY, NOT EXECUTED
|
||||
|
||||
### Existing grant mechanism (Phase 4)
|
||||
- **Supported profile-only method: YES** — `Store.grant_unopened_pack(pack_id)`
|
||||
(`fut_store.py:635-643`): validates the id is in `PACK_CATALOG`, appends to
|
||||
`unopenedPackIds`, persists; reverts via `consume_unopened_pack` (`:623-633`).
|
||||
Modifies **only** profile state; cleanly reversible.
|
||||
- **BUT restart IS required to take effect.** `Store.load()` caches `self._p`
|
||||
(`:370-372`); **no route calls `grant_unopened_pack` or `select_account`**, and
|
||||
`select_account` only re-reads on a persona *change*. So an out-of-process grant or
|
||||
a raw profile-file edit writes disk but the **running server keeps serving its
|
||||
cached `unopenedPackIds`** until the process reloads. There is no SIGHUP/reload
|
||||
endpoint. Therefore any profile change needs a container restart to be observed.
|
||||
|
||||
### Experiment A — Non-empty My Packs (PREFERRED; least invasive)
|
||||
- **State change:** set the profile's `unopenedPackIds` to `[70]` (edit
|
||||
`…/state/accounts/33068179/fifa17_profile.json`, or call
|
||||
`STORE.grant_unopened_pack(70)`), **store code/config unchanged**.
|
||||
- **Restart required?** **YES** — profile cache (above). Profile-only; no code edit.
|
||||
- **Rollback:** set `unopenedPackIds` back to `[]` (or `consume_unopened_pack(70)`),
|
||||
restart. (Also restore `nextItemId`/coins only if a pack is actually opened — the
|
||||
grant alone touches only `unopenedPackIds`.)
|
||||
- **Expected `purchasegroup` difference:** sentinel 65534 GONE; instead one active
|
||||
pack id 70 in the `mypacks` group (`state:active`, `unopened:true`); hub/credits
|
||||
report `recoveredPacks:1`.
|
||||
- **Expected client observation:** if H3 is correct, the immediate *"pack not
|
||||
available"* dialog should NOT fire (or behavior changes) and My Packs should show a
|
||||
real pack. If the dialog still fires identically, H3 is weakened and the cause is
|
||||
elsewhere (packed-exe predicate / another field).
|
||||
|
||||
### Experiment B — Suppress the empty sentinel (only if A is inconclusive)
|
||||
- **Change:** in `store_catalog` (`:3428-3446`), when `unopened_packs()` is empty, do
|
||||
NOT append pack 65534 (emit no `mypacks` entry). **Code change ⇒ restart. NOT
|
||||
authorized.** Narrowest patch: guard/remove the `if not owned_ids:` sentinel block.
|
||||
- **Purpose:** distinguishes "the inactive sentinel is selected and fails" (A already
|
||||
tests the inverse) from "an absent `mypacks` group causes a different failure"
|
||||
(the original author's stated fear at `:3429-3431`).
|
||||
|
||||
### Experiment C — Alternate sentinel representation (design only)
|
||||
- If evidence later shows FIFA expects an empty `mypacks` group represented
|
||||
differently (e.g. present-but-not-a-pack, or `state` other than `inactive`), adjust
|
||||
the sentinel shape. **DESIGN ONLY; DO NOT IMPLEMENT.** No current evidence specifies
|
||||
the correct empty-group representation (see §11.4-11.5).
|
||||
|
||||
Note: both A and B require a restart (A for the profile cache, B for the code). A is
|
||||
strictly less invasive (profile-only, clean rollback, no code change) and is the
|
||||
preferred next experiment. Neither is authorized yet.
|
||||
|
||||
---
|
||||
|
||||
## 14. Experiment A — Non-empty My Packs (EXECUTED 2026-08-13) — OBSERVED
|
||||
|
||||
Authorized controlled test of H3: change ONLY the profile's `unopenedPackIds`
|
||||
(`[] → [70]`), restart the FUT backend once, capture a genuine FIFA store request,
|
||||
then roll back. No store code/config/flags/PACK_CATALOG/pack-70/sentinel-code
|
||||
changed; FIFA not modified/restarted; operator drove the client.
|
||||
|
||||
### 14.1 Baseline profile state
|
||||
- Path: `fifa17-recon/docker/state/accounts/33068179/fifa17_profile.json` (persona
|
||||
33068179/CAGE; proven live: `STORE.path` in-process = `/state/accounts/33068179/
|
||||
fifa17_profile.json`, coins 29876776 matching the live `/user/credits`).
|
||||
- `unopenedPackIds == []`. Original SHA-256
|
||||
`39bb3e833fa55287d8516815ba3a717b41c0f0c7a7c41d20503f3a55c65cc6e7`. Backup:
|
||||
`/tmp/fifa17_profile.33068179.ORIG.20260813T001228Z.json` (same hash).
|
||||
|
||||
### 14.2 State change
|
||||
- Narrow anchored edit of line 97070 only: ` "unopenedPackIds": [],` →
|
||||
` "unopenedPackIds": [70],`. Diff vs backup = exactly one line; semantic diff =
|
||||
only key `unopenedPackIds` (`[] → [70]`), all other 24 keys identical. Modified
|
||||
SHA-256 `2b5760baa265f320904de2d23fd6ab374733efe74cb0756c3be39c083bce4ac8`.
|
||||
(Used the direct edit rather than `grant_unopened_pack(70)` to avoid whole-file
|
||||
reserialization; net semantic effect is identical.)
|
||||
|
||||
### 14.3 Restart
|
||||
- `docker restart openfut-fut-backend` (Pid 1398009→1548312, StartedAt
|
||||
2026-08-12T16:40:52Z → 2026-08-13T00:13:54Z). Bridge/Core and Rust hosts untouched.
|
||||
This container bundles blaze/roster/utas/pow (per `entrypoint.sh`); all rebound.
|
||||
- Post-restart in-process check: `STORE.load()['unopenedPackIds'] == [70]`;
|
||||
`store_catalog(None)` → pack 70 present, sentinel 65534 absent.
|
||||
|
||||
### 14.4 Genuine FIFA capture
|
||||
- Marker `2026-08-13T00:14:24Z`. Single request after it (unambiguous):
|
||||
`00:14:58 GET /ut/game/fifa17/store/purchasegroup/all?ppInfo=true → 200`.
|
||||
**No `/store/transaction`, no `/purchased/items`** in the window (pack 70 not opened).
|
||||
- Full body recovered from the running code over the live [70] profile; its
|
||||
`[:200]` matches the genuine logged 200-byte prefix byte-for-byte (verified MATCH).
|
||||
Preserved at `docs/evidence/store_purchasegroup_capture_mypacks70_2026-08-12.json`.
|
||||
|
||||
### 14.5 Client-observed behavior (operator report)
|
||||
1. Store remains open: **YES**.
|
||||
2. "The pack you've selected is currently not available": **NO (gone)**.
|
||||
3. My Packs category / unopened reward pack visible: **YES**.
|
||||
4. Returned to FUT Hub: yes (normal navigation; not forced by an error dialog).
|
||||
|
||||
### 14.6 Response diff (baseline 2026-08-12 vs experiment)
|
||||
Only difference across all 5 entries:
|
||||
- **Removed:** id `65534` (`state:inactive`, `displayGroup:mypacks`, `description:""`).
|
||||
- **Added:** id `70` (`state:active`, `displayGroup:mypacks`,
|
||||
`description:"Reward Special Players Pack"`, `unopened:true`).
|
||||
- Packs 1/5/6/7 byte-identical. Classification: **all EXPECTED FROM UNOPENED PACK
|
||||
STATE; nothing UNEXPECTED.**
|
||||
|
||||
### 14.7 H3 assessment — **SUPPORTED**
|
||||
65534 disappeared AND pack 70 replaced it as a real My Packs entry AND the
|
||||
"pack not available" / store-exit behavior disappeared → per the pre-registered
|
||||
criterion, **H3 is strongly SUPPORTED**. The failure is tied to the My Packs group
|
||||
content when the profile owns zero unopened packs.
|
||||
|
||||
Sub-hypothesis resolution:
|
||||
- **H3c (failure unrelated to My Packs): RULED OUT.** A My-Packs-only profile change
|
||||
(no store code/config change) eliminated the failure.
|
||||
- **H3a (the inactive sentinel object itself is the trigger) vs H3b (empty My Packs
|
||||
state generally is the trigger): NOT DISTINGUISHED by Experiment A.** The change
|
||||
simultaneously (i) removed the inactive sentinel and (ii) supplied a real active
|
||||
owned pack. Either "presence of the inactive/empty sentinel" or "absence of any
|
||||
real owned pack" could be the cause. Distinguishing them requires Experiment B
|
||||
(empty `unopenedPackIds` AND suppress the sentinel so `mypacks` has no entry): if
|
||||
that also fixes it → H3a (sentinel object was the problem); if it re-breaks or
|
||||
changes → H3b (empty My Packs itself is the problem). Experiment B is a code change
|
||||
(restart-gated) and remains unauthorized.
|
||||
|
||||
### 14.8 Rollback verification
|
||||
- Profile restored from backup → SHA-256 `39bb3e83…` == original (byte-identical);
|
||||
`unopenedPackIds == []`. Disk was unmutated during the test (still `2b5760ba…`
|
||||
before rollback → store reads don't persist; pack 70 never opened).
|
||||
- `docker restart openfut-fut-backend` (Pid 1549503, StartedAt 2026-08-13T00:16:56Z).
|
||||
Post-restart in-process: `unopenedPackIds == []`, sentinel 65534 present again,
|
||||
pack 70 absent → runtime baseline restored. Bridge/Core untouched.
|
||||
|
||||
**H3 status: SUPPORTED (H3c ruled out; H3a vs H3b open).** No permanent fix
|
||||
implemented.
|
||||
|
||||
---
|
||||
|
||||
## 15. Experiment B — Empty My Packs Without Sentinel (EXECUTED 2026-08-13) — OBSERVED
|
||||
|
||||
### 15.1 Purpose
|
||||
Distinguish **H3a** (the synthetic inactive sentinel 65534 itself is the trigger)
|
||||
from **H3b** (FIFA cannot tolerate an empty My Packs state even without a sentinel).
|
||||
Hold `unopenedPackIds == []` constant; change ONLY: sentinel 65534 emitted →
|
||||
suppressed. Authorized TEMPORARY code change, reverted after test.
|
||||
|
||||
### 15.2 Baseline
|
||||
Profile `unopenedPackIds == []` (SHA-256 `39bb3e83…`, unchanged throughout). Running
|
||||
code before patch = `c89d43ea…` (host repo == container copy). Sentinel 65534 emitted.
|
||||
|
||||
### 15.3 Temporary patch — **TEMPORARY EXPERIMENT B PATCH, NOT A PERMANENT FIX**
|
||||
Applied to the **container** copy `/app/tools/utas_server.py` only (the container
|
||||
mounts `/state`, not `/app`; host repo `fifa17-recon/tools/utas_server.py` was NOT
|
||||
edited — its git diff stayed empty). Single line, `store_catalog` (line 3428):
|
||||
```
|
||||
- if not owned_ids:
|
||||
+ if False: # TEMP EXPERIMENT B PATCH -- suppress synthetic sentinel 65534 (NOT A PERMANENT FIX)
|
||||
```
|
||||
Semantic effect: when `unopened_packs()` is empty, append nothing (no sentinel, no
|
||||
replacement object). Normal packs 1/5/6/7 (appended earlier) unchanged. Diff vs the
|
||||
backed-up original = exactly this one line. Patched code SHA-256 `5bb8fca9…`.
|
||||
|
||||
### 15.4 Restart verification
|
||||
`docker restart openfut-fut-backend` (Pid 1549503→1551026, StartedAt
|
||||
2026-08-13T00:20:45Z). The writable-layer edit survived the restart; running
|
||||
`/app/tools/utas_server.py` = `5bb8fca9…` (patched). In-process: `unopenedPackIds ==
|
||||
[]`; `store_catalog` → 4 packs {1,5,6,7}, **no 65534, no 70, no `mypacks` entry**.
|
||||
Flags unchanged (`DISPLAYGROUP=ON`, `GROUPID=OFF`). Bridge/Core/Rust untouched.
|
||||
|
||||
### 15.5 Genuine FIFA capture
|
||||
Marker `2026-08-13T00:20:55Z`. Request sequence (operator opened the store):
|
||||
`00:21:05 GET /hub` → `00:21:07 GET /user/credits` → `00:21:07 GET
|
||||
/store/purchasegroup/all?ppInfo=true → 200`. **No `/store/transaction`; no further
|
||||
requests** (client crashed after receiving the store body). Full body recovered from
|
||||
the running patched code; `[:200]` matches the genuine logged prefix byte-for-byte
|
||||
(verified MATCH). Preserved at
|
||||
`docs/evidence/store_purchasegroup_capture_empty_no_sentinel_2026-08-12.json`
|
||||
(4 packs {1,5,6,7}, no `mypacks` group).
|
||||
|
||||
### 15.6 Client behavior (operator report)
|
||||
**The game CRASHED** on opening the store. Not the baseline dialog; a hard crash. No
|
||||
`/store/transaction` was issued.
|
||||
|
||||
### 15.7 Three-way response comparison
|
||||
| capture | ids present | `mypacks` group entry | client outcome |
|
||||
|---|---|---|---|
|
||||
| Baseline (`…_2026-08-12.json`) | 1,5,6,7,**65534** | 65534 `inactive`, desc "" | "pack not available" dialog → Hub |
|
||||
| Exp A (`…_mypacks70_…json`) | 1,5,6,7,**70** | 70 `active`, "Reward Special Players Pack" | **works** — store open, My Packs visible |
|
||||
| Exp B (`…_empty_no_sentinel_…json`) | 1,5,6,7 | **none** | **CRASH** |
|
||||
Normal packs {1,5,6,7} identical across all three.
|
||||
|
||||
### 15.8 H3a / H3b verdict
|
||||
- **H3a (sentinel object itself is the trigger): CONTRADICTED.** Removing the
|
||||
sentinel did NOT restore the store; it produced a *worse* outcome (crash). If the
|
||||
sentinel object were the sole cause, its removal would yield a working store (it
|
||||
did not).
|
||||
- **H3b (FIFA cannot tolerate an empty My Packs state): SUPPORTED.** Only Exp A — a
|
||||
real **active** owned pack in `mypacks` — worked. Both the inactive sentinel
|
||||
(graceful "pack not available" dialog) and the total absence of any `mypacks` entry
|
||||
(crash) fail. The sentinel is a **load-bearing workaround** that *downgrades* the
|
||||
failure from a crash to a dialog but does not fix it.
|
||||
- **Pre-registered-rule nuance:** Exp B produced a *distinct* failure (crash), which
|
||||
the pre-registered rules classify as **B3 (different failure)** rather than the
|
||||
exact B2 dialog. Documented as such: the crash is a THIRD failure mode. It still
|
||||
resolves the question — it rules out H3a and supports H3b — but the specific
|
||||
outcome (crash, not the same dialog) is stronger than B2 anticipated. Not forced
|
||||
into a clean binary beyond what the evidence shows.
|
||||
|
||||
**Does FIFA tolerate an empty My Packs without the sentinel? NO — it crashes.**
|
||||
|
||||
### 15.9 Rollback verification
|
||||
- Container `/app/tools/utas_server.py` restored from backup → SHA-256 `c89d43ea…`
|
||||
== pre-experiment (byte-identical); the `if False:` patch fully removed.
|
||||
- `docker restart openfut-fut-backend` (final Pid 1551901,
|
||||
StartedAt 2026-08-13T00:22:04Z). In-process: `unopenedPackIds == []`, sentinel
|
||||
65534 emitted again, pack 70 absent; `DISPLAYGROUP=ON`, `GROUPID=OFF`.
|
||||
- Host repo `fifa17-recon/tools/utas_server.py` never edited (git diff empty, SHA-256
|
||||
`c89d43ea…`). Profile unchanged (`39bb3e83…`). Bridge/Core/Rust untouched.
|
||||
|
||||
### 15.10 Likely permanent fix (REPORT ONLY — not implemented)
|
||||
Evidence: the store's `mypacks` group must contain a **valid, active, openable owned
|
||||
pack**; both an inactive sentinel and an absent group fail (dialog / crash). The only
|
||||
working configuration observed is a genuine active owned pack (Exp A). Candidate
|
||||
directions (report only, each needs design + authorization):
|
||||
1. Ensure the profile always owns ≥1 legitimate active unopened pack while the store
|
||||
is shown (e.g. keep a real reward pack such as id 70 granted), so `mypacks` is
|
||||
never empty — this matches the only known-working state but changes economy state
|
||||
and needs a lifecycle policy (what happens after the user opens it).
|
||||
2. Change what `store_catalog` advertises so FIFA never lands on / requires a
|
||||
`mypacks` group when there are zero owned packs (client-compatible empty-store
|
||||
representation) — the correct representation is UNKNOWN; neither current option
|
||||
(inactive sentinel / no group) is it, so this needs new client-side RE before
|
||||
implementation.
|
||||
Recommendation: do NOT simply delete the sentinel (Exp B proves that crashes). No fix
|
||||
implemented.
|
||||
|
||||
**H3 status: SUPPORTED. H3a CONTRADICTED, H3b SUPPORTED (Exp B crash = third failure
|
||||
mode; empty My Packs is the root problem). Sentinel is a load-bearing workaround.**
|
||||
|
||||
---
|
||||
|
||||
## 16. Experiment C′ — Active Non-Openable Placeholder (EXECUTED 2026-08-13) — OBSERVED
|
||||
|
||||
Question: can the required `mypacks` group be kept structurally valid with an
|
||||
**active** placeholder that stays impossible to open/purchase? Change exactly one
|
||||
field of sentinel 65534: `state "inactive" → "active"`. Profile untouched
|
||||
(`unopenedPackIds==[]`); 65534 kept absent from `PACK_CATALOG`.
|
||||
|
||||
### 16.1 Server-side safety proof (OBSERVED, code)
|
||||
65534 cannot grant value regardless of `state` — pack resolution is by
|
||||
`pack_by_id(id)` over `PACK_CATALOG` (ids 1,5,6,7,70), independent of the display
|
||||
`state`:
|
||||
- `store_buy` (PUT `/store/transaction`, `:3460-3465`): `pack_by_id(65534)=None` →
|
||||
`if not pack: return 200, {}` (no `open_pack`, no coin change).
|
||||
- `purchased_items` (POST, `:3494-3496`): `pack_by_id(65534)=None` →
|
||||
`return 200, {"itemData": STORE.last_pack()}` (stale prior items only; no new
|
||||
grant, no `open_pack`, no `consume_unopened_pack`).
|
||||
- `open_pack`/`consume_unopened_pack` are unreachable for 65534 (pack resolves to
|
||||
None first). Precondition PASSED.
|
||||
|
||||
### 16.2 Baseline / patch
|
||||
Baseline: profile `39bb3e83…`, code `c89d43ea…` (host==container), sentinel
|
||||
`inactive`/`mypacks`, 65534∉catalog, flags `DISPLAYGROUP=ON`/`GROUPID=OFF`.
|
||||
Temporary container-only patch (host repo untouched), line 3444:
|
||||
`empty["state"] = "inactive"` → `empty["state"] = "active"`. Diff vs original = this
|
||||
one line; patched code `e1a4e1dc…`. **TEMPORARY EXPERIMENT C′ PATCH — NOT A PERMANENT
|
||||
FIX.**
|
||||
|
||||
### 16.3 Restart / runtime
|
||||
`docker restart openfut-fut-backend` (Pid 1556305, StartedAt 00:38:51Z). Running code
|
||||
`e1a4e1dc…`; `unopenedPackIds==[]`; sentinel `65534 state=active unopened=False
|
||||
dg=mypacks desc=""`; 65534∉catalog; normal packs 1/5/6/7 active; 70 absent; flags
|
||||
unchanged.
|
||||
|
||||
### 16.4 Genuine FIFA capture
|
||||
Marker `2026-08-13T00:38:53Z`. FIFA relaunched (Exp-B crash had closed it) → booted to
|
||||
hub. Three genuine store fetches: `00:39:44`, `00:40:26`, `00:41:23`
|
||||
(`GET /store/purchasegroup/all?ppInfo=true → 200`), reconstructed body prefix-matches
|
||||
the logged 200-byte prefix (verified). Saved
|
||||
`docs/evidence/store_purchasegroup_capture_active_placeholder_2026-08-12.json`.
|
||||
C′-vs-baseline full JSON diff = **only** `65534.state: "inactive" → "active"`.
|
||||
|
||||
### 16.5 UI observation (operator report)
|
||||
1. **No crash** (game launched to hub).
|
||||
2. **No "pack not available" dialog.**
|
||||
3. Store remains open.
|
||||
4. My Packs tab **not shown while inside the Store (Browse Packs)**.
|
||||
5. Via the FUT-hub **My Packs** menu: **one pack tile with no cover, "0 items, 0
|
||||
bronze, 0 rares"** (the placeholder renders as a visible empty pack).
|
||||
6. Navigation: from the hub **My Packs** menu → Bronze/Gold/Special reachable; but
|
||||
from the **Browse Packs** (Store) entry, Bronze/Gold/Special are **not reachable
|
||||
until My Packs is opened first**.
|
||||
|
||||
### 16.6 Passive request sequence (OBSERVED)
|
||||
Boot: `.../accountinfo → /ut/auth → settings → phishing → match/reset → userMassInfo
|
||||
→ PUT store/transaction/0 → hub …`. The single `/store/transaction` is the routine
|
||||
**boot** call with body `{"state":"TRANSACTIONCANCEL"}` → `200 {}` (no packId), fired
|
||||
at 00:39:39 **before** any store fetch. Across all three store opens: **no
|
||||
`/store/transaction`, no `/purchased`, and no request referencing 65534.** The active
|
||||
placeholder did NOT cause FIFA to auto-submit any transaction/open.
|
||||
|
||||
### 16.7 Availability result / verdict
|
||||
**Result C1 (strong positive) — ACTIVE PLACEHOLDER HYPOTHESIS SUPPORTED, with UX
|
||||
caveats.** `state` participates materially: with `state:"active"` the group exists
|
||||
(no crash, as in baseline) AND the availability path is satisfied (no
|
||||
"pack not available" dialog, unlike baseline). So **C2 is refuted** — `state` is a
|
||||
deciding field for the dialog. But it is **not a clean permanent fix**:
|
||||
- the placeholder renders as a **visible empty pack tile** ("0 items"), i.e. a fake
|
||||
pack a user could try to open (server-safe: opening → no-op `{}` / stale
|
||||
`last_pack`, but confusing UX);
|
||||
- **navigation caveat #6**: from Browse Packs the other categories are gated behind
|
||||
opening My Packs first — an UNEXPECTED behavior not present with a genuine owned
|
||||
pack (Exp A).
|
||||
|
||||
### 16.8 Rollback verification
|
||||
Container code restored from backup → `c89d43ea…` (== host, == pre-experiment); the
|
||||
`state` change removed. `docker restart` (Pid 1557831, StartedAt 00:43:04Z).
|
||||
In-process: `unopenedPackIds==[]`, sentinel `state=inactive`, 65534∉catalog, 70
|
||||
absent, flags `DISPLAYGROUP=ON`/`GROUPID=OFF`. Host repo `utas_server.py` never edited
|
||||
(`c89d43ea…`, git diff empty). Profile `39bb3e83…` unchanged. Bridge/Core untouched.
|
||||
|
||||
### 16.9 Implications for the client contract
|
||||
`state:"active"` satisfies the pack-availability predicate (no dialog) while the
|
||||
group's existence prevents the crash — so an active non-openable placeholder is the
|
||||
first representation that neither crashes nor shows the dialog. However it exposes a
|
||||
**visible empty "pack"** and a **Browse-Packs navigation gate** (#5/#6), so it is NOT
|
||||
adopted. **Permanent fix NOT established.** Open follow-ups: (a) what happens if the
|
||||
user explicitly selects/opens the active placeholder (a later controlled test —
|
||||
server-safe per §16.1 but UX-unknown); (b) whether a count-gated My-Packs default or a
|
||||
representation that avoids rendering a fake tile can remove the empty-tile/navigation
|
||||
artifacts. Do NOT adopt `state:"active"` as the fix on this evidence alone.
|
||||
|
||||
---
|
||||
|
||||
## 17. Explicit Active-Placeholder Selection Test (EXECUTED 2026-08-13) — OBSERVED
|
||||
|
||||
**Purpose:** with the C′ active placeholder in place, characterize what happens when
|
||||
the user *explicitly opens* the empty 65534 My-Packs tile (the last open backend-side
|
||||
question).
|
||||
|
||||
**Server safety proof (re-confirmed, code `c89d43ea`):** `pack_by_id(65534)=None`;
|
||||
65534∉PACK_CATALOG∉unopenedPackIds. `store_buy`→`200 {}`; `purchased_items`→
|
||||
`200 {"itemData": last_pack}` (stale). No inventory/coin/profile mutation possible.
|
||||
|
||||
**Temporary C′ state:** container-only one-line patch `65534.state "inactive"→"active"`
|
||||
(patched `e1a4e1dc…`), restart (Pid 1560774). `unopenedPackIds=[]`, 65534
|
||||
active/mypacks/∉catalog, normal packs unchanged, flags ON/OFF, host code + profile
|
||||
unchanged. Marker `2026-08-13T00:53:09Z`.
|
||||
|
||||
**Manual selection behavior (operator report):** opened FUT → My Packs → the empty
|
||||
placeholder tile visible → selected/opened it ONCE:
|
||||
1. Dialog: **YES**. 2. Exact text: **"This pack is no longer available"**.
|
||||
3. Stays in My Packs: yes. 4. After closing the dialog → returns to My Packs.
|
||||
5. Then navigates back to the FUT Hub successfully. 6. **No crash.** 7. No spinner.
|
||||
8. **Navigation remains fully usable afterward.**
|
||||
|
||||
**Genuine request sequence (OBSERVED, marker `00:53:09Z`):** boot (`…/auth →
|
||||
userMassInfo → PUT store/transaction/0 {"state":"TRANSACTIONCANCEL"}→200 {} → hub →
|
||||
credits → purchasegroup`) then navigation (`hub→credits→purchasegroup` ×2 for the
|
||||
store/My-Packs views). **The explicit tile selection generated NO server request** —
|
||||
no `/store/transaction`, no `/purchased/items`, and NO reference to 65534 anywhere.
|
||||
The "no longer available" verdict is rendered **client-side**.
|
||||
|
||||
**Result class: S1 — pure client-side rejection.**
|
||||
|
||||
**Post-test profile/economy integrity (OBSERVED):** coins 29876776, nextItemId
|
||||
100004837, items 1995, purchased 0, `unopenedPackIds` `[]`, `last_pack` empty — ALL
|
||||
unchanged vs pre-test; 65534 not persisted in items or unopenedPackIds; profile
|
||||
SHA-256 `39bb3e83…` byte-identical. **Zero mutation.**
|
||||
|
||||
**UX assessment:** crash-safe ✓, economy-safe ✓ (no request even sent), navigation
|
||||
recoverable ✓. Blemishes: a **visible empty "0 items" tile**, a **"This pack is no
|
||||
longer available" dialog on explicit click**, and (from §16.5) the **Browse-Packs
|
||||
navigation gate** (must open My Packs first). Notably this is a *strict improvement*
|
||||
over the inactive-sentinel baseline, which throws "pack not available" immediately on
|
||||
STORE OPEN and bounces to the Hub; the active placeholder only errors if the user
|
||||
deliberately clicks the empty tile, and recovers cleanly.
|
||||
|
||||
**Active-placeholder verdict: MARGINALLY ACCEPTABLE.** Safe (crash + economy) and
|
||||
usable, but visibly imperfect (fake tile + click-dialog + browse nav gate). Not
|
||||
UNACCEPTABLE (no crash/economy risk, recoverable); not fully ACCEPTABLE (user-visible
|
||||
defects).
|
||||
|
||||
**Permanent-fix decision: P2.** The active sentinel technically works and is safe, but
|
||||
its UX is poor and — per Candidate F (CONTRADICTED) — **no backend-only *clean*
|
||||
solution exists** (the store's My-Packs resolution is Scaleform/movie-driven, not
|
||||
server-gated). Recommendation: keep the active placeholder as an optional/temporary
|
||||
backend compatibility mode (strictly better than the current inactive-sentinel
|
||||
baseline) and pursue a **client-side** fix for a fully clean zero-pack experience
|
||||
(hiding the fake tile / suppressing the forced My-Packs resolution). NOT implemented.
|
||||
|
||||
**Rollback verification:** container code restored to `c89d43ea…` (== host, ==
|
||||
pre-experiment), sentinel back to `inactive`; `docker restart` (Pid 1562665, StartedAt
|
||||
00:59:04Z); `unopenedPackIds=[]`, 65534 inactive/∉catalog, 70 absent, flags ON/OFF;
|
||||
host `utas_server.py` never edited; profile `39bb3e83…` unchanged. Bridge/Core
|
||||
untouched.
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# FIFA 17 card-table provenance manifest
|
||||
# Source (authoritative): 10.10.0.105:/home/alex/Documents/OpenFUT/fifa17-recon/data/tables/
|
||||
# Dest (this repo): fifa17-recon/data/tables/
|
||||
# Verified 2026-08-12: source and dest byte-identical (sha256), order-independent.
|
||||
# Combined hash-of-hashes: 10f239add919089354d8dbff873fc9737b0a0f80f6ac41b1aa2a096c0ec8d331
|
||||
# 31 fcc_*.json + 5 staff tables = 36 files. Files are DECODED tables:
|
||||
# each carries {table, source, rowcount, schema[], rows[]}; card instances live in rows[].
|
||||
#
|
||||
0d1c9af7ae654c3e4363f18bb89bad03a0631056d36425c84b9a680fa989618c fcc_managerbonusvalues.json
|
||||
0e5d5309cd1d9322476f8047fc6eaf4a88f4f19211b8ea01fe4d28ddd3733134 fcc_healingcards.json
|
||||
1da80e390169ebb8ee8a6543e14b1191d9151f675797f01e23628f6c24d434c5 fcc_misccards.json
|
||||
2212b0ee8d962f0fb6bd346bee35fff6566e22539e397cc2e42bb6efd4dc3ad3 fcc_textposvalues_hd.json
|
||||
2a8e22ddb000b2c08f1a3e5eb47bc56ecf43f733ce6e7519498d332e4f439746 headcoachcards.json
|
||||
3a323e1c0688a4068ccd21be0c9d8e88a875ab10ce2158d3aed79fc14e65f0fb fcc_chemlinkcalc.json
|
||||
4a60bc4c0d8cb8d2b903e152a3dd5302348753568630818fde059b2de41f82ab fcc_leaguelogos.json
|
||||
5304114078200da4564d33612c955598f12a44bdf52f18274184b98922229b8b fcc_GrandStandPlayers.json
|
||||
5503291e381fee5008120615cd6d30a732b97636d4694a88f943eb14cb992741 fcc_leaguelogostickers.json
|
||||
550739c124ca915fb294954afe3d9d04fb7d1faf2b0c96930b2dcdb1bfe7ac8f fcc_formationcardspositions_kc.json
|
||||
5a5aabec1d40ffa21b8effb84f79e4b788cb42352aa592d71d95eba1db0d209e fcc_trainingcards.json
|
||||
6476e396f166905857d2ada4f12cc37645ca42efdca121e8e74270fbf7422336 managercards.json
|
||||
6546f602024973e20d04522a857c6c243473a177cab5b3be8400390651a42fab fcc_navcoords_hd.json
|
||||
6b0209647383e4e940d2af2c3bbb2185a4aac7ac0e799fe6b50ae52e7625710d fcc_contractcards.json
|
||||
6c52c83aafd9d9d3406e21c656762ac5cc0ba4522002f424e5593430bc5190f5 physiocards.json
|
||||
765687d7f1e5c6c989adf45b174a0fdab0f65597c83132304b53b9f859c02586 fcc_stadium.json
|
||||
79e50b07eecc47a0edf4d2a87782e904785e653937698cc712258a82fdf8b079 fcc_formationcardspositions_hd.json
|
||||
7d36e0fbb9349eabd4267215bacbe29d78ff621deeb8cab3380dcac72c535eb9 fcc_preferredformationcalcmid.json
|
||||
8122a4070901662fac97f675a3e4194dd5fd7e02194fdab204989af42676e268 fitnesscoachcards.json
|
||||
828f8b90241672b9f62a9bbd3cb219a1d3bd8856bd160cc46284f2958e08f7d4 fcc_discardcoins.json
|
||||
852bb82ed373881373d8610e6f4f2ca4406bac13da4bda9d6abba693d7cafc56 fcc_myclubscategories.json
|
||||
974dcbbe6a46c02dc97c77df6c270c9a7f09ba23bee23005ecf95fd114ee66a7 gkcoachcards.json
|
||||
9aa4b3b3f226202b21d2e9f96a1508ecce56abba64372099e090df101fce5eeb fcc_nationcalc.json
|
||||
9b6797991520c05f7448b28e66d160f96afc73eefd661ed8272b0a093b6ba89f fcc_kitcards.json
|
||||
9e8e6595fa8d3bcf963eb25bd9f9ea5d131aa92ed0e7e4ae3089adf5e1d55927 fcc_preferredformationcalcst.json
|
||||
a4e8ac2ba0a6db45f1f59fe384fbd39a8cee8fb72a72f846e52daca44f1c7ff9 fcc_myclubs.json
|
||||
bbf405e3a63b6fd03da1237b8b118764c57a40c797faf85d1e4691a1c95a840e fcc_balls.json
|
||||
ca1184bd85cff3308af0104077feda4357ef483fae6335fa964922eea4e94330 fcc_navcoords_kc.json
|
||||
ca3e4ab0f7892aac7473b774de4699c067c54647ca4a82cdd5fd1108c56ed894 fcc_badgecards.json
|
||||
ccdda8ad0a15f73a056fa336abde8739b346d12b78cb8adbea0f0677487bb598 fcc_preferredpositioncalc.json
|
||||
dbf95bddd456137e4b90a44bdd1f637458f4846ecd5c3ba6747d3f069c3f590f fcc_textposvalues_kc.json
|
||||
dd8c2c860b18d999877c37e0f63dac64ef7bb57bff5a2173960cde71242f9a34 fcc_bonusvalues.json
|
||||
df5b997b153941a5bb760ad5bb0fb23dc16cf8612b300559be23ac929b55eec6 fcc_leagues.json
|
||||
e4619db324a7848639a8ba53f513cf3ea153eeaf698de05d71e56c319b3cc424 fcc_coinrewards.json
|
||||
e6b1ca3ecb7c3923d77e73bda2e6c3f9794b9158354d566112f7979b33b4422c fcc_preferredformationcalcgk.json
|
||||
f54814d61b72dd2b6186e9e414df4fbfbdbea1732da6a4622f001a1ff03bc12a fcc_preferredformationcalcback.json
|
||||
@@ -1,66 +0,0 @@
|
||||
# Live UTAS wire captures — 2026-08-15
|
||||
|
||||
Fresh sanitised FIFA 17 UTAS wire, captured during the post-P1 staging A/B on a
|
||||
**real FIFA 17 client** (`10.10.0.105`) driving the isolated post-P1 candidate
|
||||
backend (`10.10.0.121`, host bin `fda40d12`, Core `fbb54ea`). Production untouched.
|
||||
|
||||
This rebuilds the primary-capture corpus that was lost to `.gitignore` (Known Issues
|
||||
#200 / "take sanitised captures on the next live FIFA run and commit them").
|
||||
|
||||
## Files
|
||||
- `utas-requests.sanitised.txt` — 10 real client requests (`---`-separated).
|
||||
- `utas-responses.sanitised.txt` — 12 responses.
|
||||
|
||||
Captured with `tcpdump` on the container netns (UTAS is plain HTTP on `:8099`),
|
||||
reassembled + split + **sanitised** by `openfut-staging/extract_http.py`. Redacted:
|
||||
`X-UT-SID`/`sid` (`<SID>`), `authCode` (`<AUTH>`), `deviceId` (`<DEV>`), `macAddress`
|
||||
(`<MAC>`), and any 32+char hex token (`<TOKEN>`). The raw `.pcap` is intentionally
|
||||
NOT committed (it is unsanitised).
|
||||
|
||||
## Route contracts captured (verbatim shapes)
|
||||
|
||||
### POST /openfut/account/sync (launcher control-plane, Python-served)
|
||||
Request (launcher → backend):
|
||||
```json
|
||||
{"personaId":33068179,"personaName":"CAGE","level":1,"experience":0,
|
||||
"experienceMax":1000,"accountFunds":0,"accountFundsCap":100000}
|
||||
```
|
||||
Response:
|
||||
```json
|
||||
{"account":{"personaId":33068179,"personaName":"CAGE","clubName":"OpenFUT",
|
||||
"clubAbbr":"OFC","level":1,"experience":0,"experienceMax":1000,"accountFunds":0,
|
||||
"accountFundsCap":100000,"profilePath":"accounts/33068179/fifa17_profile.json",
|
||||
"coins":29826776,"unopenedPacks":0},"status":"OK"}
|
||||
```
|
||||
NOTE: `coins` here is Python's STALE profile value (29,826,776) — Core's authoritative
|
||||
balance at capture time was 29,859,876. The launcher's `AccountSummary` parser is
|
||||
lenient and requires only `{personaId,personaName,level,experience,accountFunds,coins,
|
||||
unopenedPacks}` (clubName/clubAbbr/etc. ignored). BLOCKER for a Rust migration is NOT
|
||||
the shape — it is that Python `account/sync` also *selects the active profile* the
|
||||
still-Python-served userMassInfo envelope depends on, so it is coupled to the
|
||||
userMassInfo hybrid and cannot migrate standalone.
|
||||
|
||||
### GET /ut/game/fifa17/store/purchasegroup/all?ppInfo=true (empty My-Packs sentinel)
|
||||
The `mypacks` group carries the synthetic sentinel pack when My Packs is empty:
|
||||
```json
|
||||
{"assetId":65534,"displayGroup":{"priority":1,"value":"mypacks"},"id":65534,
|
||||
"packType":"GOLD","packContentInfo":{"goldQuantity":0,"itemQuantity":0,...},
|
||||
"state":"active","unopened":false}
|
||||
```
|
||||
Client renders empty My Packs AS Browse Packs (no crash) — the bug-6c resolver guard.
|
||||
|
||||
### GET /ut/game/fifa17/userMassInfo (RUST_OVERLAY hybrid, full 8 KB envelope captured)
|
||||
The complete envelope is in `utas-responses.sanitised.txt`: root `pileSizeClientData` +
|
||||
`userInfo` (with `clubName`, `clubAbbr`, `established`, `accountCreatedPlatformName`,
|
||||
`currencies` [Core coins overlaid by Rust], `won/draw/loss`, `clubNameChangeAllowed`,
|
||||
`divisionOffline/Online`, `purchased`, `feature`, `reliability`, `bidTokens`, `trophies`,
|
||||
`sessionCoinsBankBalance`, `actives`, `squadList`). Field spec is authoritative in
|
||||
`fifa17-recon/tools/utas_server.py::user_info()`. This is the target shape for a future
|
||||
full-Rust userMassInfo (needs the `clubAbbr`/`established` account triad, which Core lacks).
|
||||
|
||||
### Other captured request lines
|
||||
`POST /ut/auth`, `GET user/accountinfo`, `GET settings`, `GET phishing/trusteddevice`,
|
||||
`PUT match/reset`, `GET userMassInfo`, `PUT clientdata/userHubData`,
|
||||
`PUT /ut/v2/game/fifa17/store/transaction/0`.
|
||||
|
||||
See `openfut-staging/p1p-live-2026-08-15/AB_VERDICT.md` for the full A/B result.
|
||||
@@ -1,117 +0,0 @@
|
||||
POST /openfut/account/sync HTTP/1.1
|
||||
Host: 10.10.0.121:8099
|
||||
Content-Type: application/json
|
||||
Content-Length: 131
|
||||
Connection: close
|
||||
|
||||
{"personaId":33068179,"personaName":"CAGE","level":1,"experience":0,"experienceMax":1000,"accountFunds":0,"accountFundsCap":100000}
|
||||
|
||||
---
|
||||
|
||||
GET /ut/game/fifa17/user/accountinfo HTTP/1.1
|
||||
Host: 10.10.0.121:8099
|
||||
Connection: Close
|
||||
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Accept-Encoding: gzip
|
||||
Easw-Session-Data-Nucleus-Id: 33068179
|
||||
Accept-Encoding: gzip
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
POST /ut/auth HTTP/1.1
|
||||
Host: 10.10.0.121:8099
|
||||
Content-Length: 380
|
||||
Connection: Close
|
||||
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Hash: <TOKEN>
|
||||
|
||||
{"isReadOnly":false,"priorityLevel":6,"sku":"FFA17PCC","nucleusPersonaPlatform":"pc","clientVersion":3,"nuc":33068179,"nucleusPersonaId":33068179,"nucleusPersonaDisplayName":"CAGE","locale":"en-US","regionCode":"US","deviceId":"<DEV>","macAddress":"<MAC>","method":"authcode","identification":{"authCode":"<AUTH>"}}
|
||||
|
||||
---
|
||||
|
||||
GET /ut/game/fifa17/settings HTTP/1.1
|
||||
Host: 10.10.0.121:8099
|
||||
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
X-UT-SID: <SID>
|
||||
Accept-Encoding: gzip
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
GET /ut/game/fifa17/phishing/trusteddevice?deviceId=<DEV> HTTP/1.1
|
||||
Host: 10.10.0.121:8099
|
||||
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
X-UT-SID: <SID>
|
||||
Accept-Encoding: gzip
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
PUT /ut/game/fifa17/match/reset HTTP/1.1
|
||||
Host: 10.10.0.121:8099
|
||||
Content-Length: 0
|
||||
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
X-UT-SID: <SID>
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
GET /ut/game/fifa17/userMassInfo HTTP/1.1
|
||||
Host: 10.10.0.121:8099
|
||||
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
X-UT-SID: <SID>
|
||||
Accept-Encoding: gzip
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
PUT /ut/v2/game/fifa17/store/transaction/0 HTTP/1.1
|
||||
Host: 10.10.0.121:8099
|
||||
Content-Length: 29
|
||||
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
X-UT-SID: <SID>
|
||||
|
||||
{"state":"TRANSACTIONCANCEL"}
|
||||
|
||||
---
|
||||
|
||||
PUT /ut/game/fifa17/clientdata/userHubData HTTP/1.1
|
||||
Host: 10.10.0.121:8099
|
||||
Content-Length: 53
|
||||
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
X-UT-SID: <SID>
|
||||
|
||||
{"entries":[{"key":0,"value":0},{"key":1,"value":1}]}
|
||||
|
||||
---
|
||||
|
||||
GET /ut/game/fifa17/store/purchasegroup/all?ppInfo=true HTTP/1.1
|
||||
Host: 10.10.0.121:8099
|
||||
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
X-UT-SID: <SID>
|
||||
Accept-Encoding: gzip
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"_countdown_proof": {
|
||||
"delta": -4,
|
||||
"expires_after_4s": 2216,
|
||||
"expires_first": 2220,
|
||||
"frozen": false
|
||||
},
|
||||
"auctionhouse-browse": {
|
||||
"container": "object",
|
||||
"path": "/auctionhouse",
|
||||
"status": 200
|
||||
},
|
||||
"marketdata-pricelimits": {
|
||||
"container": "array",
|
||||
"path": "/marketdata/pricelimits?defId=20801",
|
||||
"status": 200
|
||||
},
|
||||
"trade-status-filtered": {
|
||||
"container": "object",
|
||||
"path": "/trade/status?tradeIds=1000000097",
|
||||
"status": 200
|
||||
},
|
||||
"trade-status-unfiltered": {
|
||||
"container": "object",
|
||||
"path": "/trade/status",
|
||||
"status": 200
|
||||
},
|
||||
"tradePile": {
|
||||
"container": "object",
|
||||
"path": "/tradePile",
|
||||
"status": 200
|
||||
},
|
||||
"tradePile-counts": {
|
||||
"container": "object",
|
||||
"path": "/tradePile/counts",
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 15000,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 2220,
|
||||
"itemData": {
|
||||
"assetId": 20801,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 81
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 91
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 33
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"cardassetid": 20801,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 20801,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000097,
|
||||
"itemState": "forSale",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 38,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LW",
|
||||
"rareflag": 1,
|
||||
"rating": 94,
|
||||
"resourceId": 20801,
|
||||
"teamid": 243,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 1000000097,
|
||||
"tradeState": "active",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"count": 0,
|
||||
"credits": 29843976,
|
||||
"duplicateItemIdList": [],
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 1,
|
||||
"sold": 0,
|
||||
"total": 1
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
image_base=0x180000000 sections=7
|
||||
.text rva=0x00001000 vsize=0x1e3f62 rawptr=0x00000400 rawsize=0x1e4000
|
||||
.rdata rva=0x001e5000 vsize=0xa4094 rawptr=0x001e4400 rawsize=0xa4200
|
||||
.data rva=0x0028a000 vsize=0x65c08 rawptr=0x00288600 rawsize=0x54000
|
||||
.pdata rva=0x002f0000 vsize=0x132cc rawptr=0x002dc600 rawsize=0x13400
|
||||
.tls rva=0x00304000 vsize=0x9 rawptr=0x002efa00 rawsize=0x200
|
||||
.rsrc rva=0x00305000 vsize=0x4e8 rawptr=0x002efc00 rawsize=0x600
|
||||
.reloc rva=0x00306000 vsize=0x16bc4 rawptr=0x002f0200 rawsize=0x16c00
|
||||
|
||||
route table VA 0x18021df80 -> file offset 0x21d380
|
||||
|
||||
# field A field B
|
||||
0 'ut/%s/auctionhouse' 'AUCTIONHOUSE'
|
||||
1 'ut/%s/clubUser' 'CLUB_USER'
|
||||
2 'ut/%s/user/list' 'CLUB_INFO'
|
||||
3 'ut/%s/club' 'CLUB'
|
||||
4 'ut/%s/defid' 'DREAM'
|
||||
5 'ut/%s/squad' 'SQUAD'
|
||||
6 'ut/delete/%s/squad' 'DELETE_SQUAD'
|
||||
7 'ut/%s/leaderboards/options' 'LBOPTIONS'
|
||||
8 'ut/%s/leaderboards' 'LBDEFAULT'
|
||||
9 'ut/%s/activeMessage' 'PAFPRACTICE'
|
||||
10 'ut/%s' 'UT'
|
||||
11 'ut/%s/user' 'USER'
|
||||
12 'ut/delete/%s/user' 'DELETEUSER'
|
||||
13 'ut/%s/item' 'ITEMS'
|
||||
14 'ut/%s/item/resource' 'ITEMS_BY_RES'
|
||||
15 'ut/delete/%s/item' 'DELETEITEMS'
|
||||
16 'ut/%s/match' 'MATCH'
|
||||
17 'ut/%s/sbs' 'SBC'
|
||||
18 'ut/%s/tournament' 'TOURNAMENT'
|
||||
19 'ut/%s/tournament/user' 'TOURNAMENTUSER'
|
||||
20 'ut/delete/%s/tournament/user' 'TOURNAMENTQUIT'
|
||||
21 'ut/%s/season' 'SEASON'
|
||||
22 'ut/%s/season/user' 'SEASONUSER'
|
||||
23 'ut/%s/season/%%s/user' 'SEASONUSER_ALTER'
|
||||
24 'ut/%s/season/%%s/reset' 'SEASONRESET'
|
||||
25 'ut/%s/season/friendly' 'FRIENDLYSEASON'
|
||||
26 'ut/%s/purchased' 'PURCHASED'
|
||||
27 'ut/%s/store' 'STORE'
|
||||
28 'ut/%s/watchList' 'WATCHLIST'
|
||||
29 'ut/delete/%s/watchList' 'DELETEWATCHLIST'
|
||||
30 'ut/%s/tradePile' 'TRADEPILE'
|
||||
31 'ut/%s/trade' 'TRADE'
|
||||
32 'ut/delete/%s/trade' 'DELETETRADE'
|
||||
33 'ut/%s/marketdata' 'MARKETDATA'
|
||||
34 'ut/%s/clientdata' 'CLIENTDATA'
|
||||
35 'ut/auth' 'AUTH'
|
||||
36 'ut/delete/auth' 'DELETE_AUTH'
|
||||
37 'ut/%s/phishing' 'PHISHING'
|
||||
38 'ut/%s/captcha' 'CAPTCHA'
|
||||
39 'ut/%s/tfa' 'TFA'
|
||||
40 'ut/%s/squad/mode' 'SQUADMODE'
|
||||
41 'ut/%s/draft/mode' 'DRAFT'
|
||||
42 'ut/%s/champion' 'CHAMPIONS'
|
||||
43 'ut/v2/%s/store' 'V2STORE'
|
||||
44 'ut/%s/livemessage' 'LIVEMESSAGE'
|
||||
45 '' 'ADMIN'
|
||||
46 '' 'DEBUG'
|
||||
47 '' 'MAINTENANCE'
|
||||
-- table ends after 48 rows --
|
||||
@@ -1,107 +0,0 @@
|
||||
|
||||
=== tradeState (table walk) VA 0x180229e40 -> off 0x229240 ===
|
||||
'active' = 1
|
||||
'inactive' = 2
|
||||
'expired' = 3
|
||||
'closed' = 4
|
||||
-- NULL terminator after 4 rows --
|
||||
|
||||
=== type VA 0x180229c30 -> off 0x229030 ===
|
||||
'any' = -1
|
||||
'player' = 1
|
||||
'staff' = 2
|
||||
'clubInfo' = 3
|
||||
'training' = 4
|
||||
'development' = 5
|
||||
'stadium' = 6
|
||||
'ball' = 7
|
||||
-- NULL terminator after 8 rows --
|
||||
|
||||
=== zone VA 0x1802296e0 -> off 0x228ae0 ===
|
||||
'goalKeeper' = 0
|
||||
'defense' = 1
|
||||
'midfield' = 2
|
||||
'attacker' = 3
|
||||
-- NULL terminator after 4 rows --
|
||||
|
||||
=== lev VA 0x180229a60 -> off 0x228e60 ===
|
||||
'any' = 0
|
||||
'gold' = 3
|
||||
'silver' = 2
|
||||
'bronze' = 1
|
||||
-- NULL terminator after 4 rows --
|
||||
|
||||
=== pos VA 0x1802295c0 -> off 0x2289c0 ===
|
||||
'GK' = 0
|
||||
'RWB' = 2
|
||||
'RB' = 3
|
||||
'CB' = 5
|
||||
'LB' = 7
|
||||
'LWB' = 8
|
||||
'CDM' = 10
|
||||
'RM' = 12
|
||||
'CM' = 14
|
||||
'LM' = 16
|
||||
'CAM' = 18
|
||||
'RF' = 20
|
||||
'CF' = 21
|
||||
'LF' = 22
|
||||
'RW' = 23
|
||||
'ST' = 25
|
||||
'LW' = 27
|
||||
-- NULL terminator after 17 rows --
|
||||
|
||||
=== cat VA 0x180229ab0 -> off 0x228eb0 ===
|
||||
'any' = -1
|
||||
'playerGK' = 1
|
||||
'playerRD' = 2
|
||||
'playerRM' = 3
|
||||
'playerRA' = 4
|
||||
'manager' = 5
|
||||
'headCoach' = 6
|
||||
'GKCoach' = 7
|
||||
'fitnessCoach' = 8
|
||||
'physio' = 9
|
||||
'badge' = 10
|
||||
'kit' = 11
|
||||
'leagueLogo' = 12
|
||||
'playerTraining' = 13
|
||||
'GKTraining' = 14
|
||||
'position' = 15
|
||||
'playStyle' = 16
|
||||
'managerLeagueModifier' = 17
|
||||
'contract' = 18
|
||||
'fitness' = 19
|
||||
'healing' = 20
|
||||
'stadium' = 21
|
||||
'ball' = 22
|
||||
-- NULL terminator after 23 rows --
|
||||
|
||||
=== form VA 0x180229880 -> off 0x228c80 ===
|
||||
'f3412' = 23
|
||||
'f3421' = 24
|
||||
'f343' = 25
|
||||
'f352' = 27
|
||||
'f41212' = 14
|
||||
'f4231' = 3
|
||||
'f4222' = 13
|
||||
'f4312' = 6
|
||||
'f4321' = 7
|
||||
'f433' = 8
|
||||
'f4411' = 19
|
||||
'f442' = 16
|
||||
'f451' = 21
|
||||
'f5212' = 29
|
||||
'f5221' = 30
|
||||
'f532' = 31
|
||||
'f541' = 32
|
||||
'f41212a' = 15
|
||||
'f4141' = 2
|
||||
'f4231a' = 4
|
||||
'f433a' = 9
|
||||
'f433b' = 10
|
||||
'f433c' = 11
|
||||
'f433d' = 12
|
||||
'f442a' = 17
|
||||
'f451a' = 20
|
||||
-- NULL terminator after 26 rows --
|
||||
@@ -1,173 +0,0 @@
|
||||
{
|
||||
"gap": 1,
|
||||
"test": "ACTIVE seller row under itemState=forSale",
|
||||
"date": "2026-08-17",
|
||||
"disposable_card": {
|
||||
"wire_item_id": 100000178,
|
||||
"resourceId": 212188,
|
||||
"rating": 75,
|
||||
"preferredPosition": "ST",
|
||||
"copies_owned": 3,
|
||||
"discardValue": 600,
|
||||
"in_active_squad": false,
|
||||
"untradeable": false
|
||||
},
|
||||
"pre_state": {
|
||||
"club_items": 1966,
|
||||
"coins": 29843976,
|
||||
"tradePile_rows": 0,
|
||||
"tradePile_counts": {
|
||||
"count": 0,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 0,
|
||||
"sold": 0
|
||||
}
|
||||
},
|
||||
"listing_creation_route_log": "utas-host owner=RUST route=market-list POST item_id=100000178 listed=true trade_id=1000000178",
|
||||
"client_flow_observed_rust_owned": [
|
||||
"PUT /ut/game/fifa17/item (club -> trade pile move)",
|
||||
"POST /ut/game/fifa17/auctionhouse (ISStart) -> 200",
|
||||
"GET /ut/game/fifa17/tradePile (repeated)",
|
||||
"GET /ut/game/fifa17/trade/status?tradeIds=1000000178 -> requested=1 returned=1 (repeated poll)",
|
||||
"GET /ut/game/fifa17/tradePile/counts"
|
||||
],
|
||||
"wire_active_row": {
|
||||
"tradeId": 1000000178,
|
||||
"tradeState": "active",
|
||||
"itemState": "forSale",
|
||||
"startingBid": 150,
|
||||
"buyNowPrice": 200,
|
||||
"currentBid": 0,
|
||||
"bidState": "none",
|
||||
"sellerName": "CAGE",
|
||||
"sellerEstablished": 1,
|
||||
"watched": false,
|
||||
"coinsProcessed": 0,
|
||||
"atom_count": 12,
|
||||
"itemData_attributeList_entries": 6
|
||||
},
|
||||
"countdown_sample": {
|
||||
"expires": [
|
||||
3562,
|
||||
3556
|
||||
],
|
||||
"delta_secs": 6,
|
||||
"live": true
|
||||
},
|
||||
"trade_status_coherent_while_running": true,
|
||||
"trade_status_rows": [
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 200,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 3556,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 64
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 78
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 24
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 62
|
||||
}
|
||||
],
|
||||
"cardassetid": 212188,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 212188,
|
||||
"discardValue": 600,
|
||||
"fitness": 99,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"itemType": "player",
|
||||
"leagueId": 19,
|
||||
"nation": 21,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "ST",
|
||||
"rareflag": 1,
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"teamid": 112172,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 1000000178,
|
||||
"tradeState": "active",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"tradePile_counts_while_active": {
|
||||
"count": 1,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 1,
|
||||
"sold": 0
|
||||
},
|
||||
"coins_unchanged": true,
|
||||
"fix_a_inactive_rows": 0,
|
||||
"CARD_OFFERSTATE_live_decode": {
|
||||
"observed": true,
|
||||
"method": "read-only /proc/<pid>/mem via fifa17-recon-derived auction_record_probe.py; slide proven by FNV control MATCH before any other read; O_RDONLY + pread only",
|
||||
"client_pid": 29670,
|
||||
"container": "model->vt[0x120] (trade pile), rows=1, +0x60 override=1",
|
||||
"record": {
|
||||
"tradeId": 1000000178,
|
||||
"STATE_tradeState": "1(active)",
|
||||
"YOURBID_bidState": "0(none)",
|
||||
"MAX_CREDITS_buyNowPrice": 200,
|
||||
"RESERVEDPRICE_startingBid": 150,
|
||||
"currentBid": 0,
|
||||
"expires": 3457,
|
||||
"coinsProcessed": 0,
|
||||
"IS_GLOW_flagB": 0,
|
||||
"INBOX_flagA": 0,
|
||||
"IS_WATCHED": 0
|
||||
},
|
||||
"item": {
|
||||
"CARD_ID": 212188,
|
||||
"CARD_TYPE": 1,
|
||||
"tradeable": 1,
|
||||
"rating": 75,
|
||||
"CARD_OFFERSTATE_itemState": "5(forSale)",
|
||||
"pile": 0
|
||||
},
|
||||
"ab_significance": "This is the direct A/B on the only changed field. Baseline `listFS` decoded to -1(<unrecognised>) in the client's own record; candidate `forSale` decodes to 5. Every other field in the record is unchanged in kind.",
|
||||
"claim_boundary": "Proves the CLIENT DECODES the token correctly. Says nothing about the Flash action-gate term."
|
||||
},
|
||||
"ui_acceptance": {
|
||||
"operator_report": "looks right",
|
||||
"confirmed": [
|
||||
"card renders under LISTED ITEMS (not the TRANSFER LIST section)",
|
||||
"Start Price 150 and Buy Now 200 displayed correctly, Current Bid as expected",
|
||||
"Time Remaining visibly counting down",
|
||||
"card art renders normally; no malformed row, no duplicate, no premature Expired, no crash",
|
||||
"row correctly non-actionable while active (established FIFA 17 behaviour, not a fault)"
|
||||
],
|
||||
"corroboration": "the client's trade-pile container populated with exactly this row (rows=1), so the Transfer List screen bound and drew it"
|
||||
},
|
||||
"verdict": "GAP 1 CLOSED \u2014 LIVE-CONFIRMED: an active seller auction with itemState=forSale renders correctly and is decoded by the client as CARD_OFFERSTATE=5",
|
||||
"code_change_required": false
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
{"t": 1787012091, "present": true, "tradeState": "active", "expires": 3175, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012151, "present": true, "tradeState": "active", "expires": 3115, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012211, "present": true, "tradeState": "active", "expires": 3055, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012271, "present": true, "tradeState": "active", "expires": 2995, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012331, "present": true, "tradeState": "active", "expires": 2935, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012391, "present": true, "tradeState": "active", "expires": 2875, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012451, "present": true, "tradeState": "active", "expires": 2815, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012511, "present": true, "tradeState": "active", "expires": 2755, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012571, "present": true, "tradeState": "active", "expires": 2695, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012631, "present": true, "tradeState": "active", "expires": 2635, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012691, "present": true, "tradeState": "active", "expires": 2575, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012751, "present": true, "tradeState": "active", "expires": 2515, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012811, "present": true, "tradeState": "active", "expires": 2455, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012871, "present": true, "tradeState": "active", "expires": 2395, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012931, "present": true, "tradeState": "active", "expires": 2335, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787012991, "present": true, "tradeState": "active", "expires": 2275, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013051, "present": true, "tradeState": "active", "expires": 2215, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013111, "present": true, "tradeState": "active", "expires": 2155, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013171, "present": true, "tradeState": "active", "expires": 2095, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013231, "present": true, "tradeState": "active", "expires": 2035, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013291, "present": true, "tradeState": "active", "expires": 1975, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013351, "present": true, "tradeState": "active", "expires": 1915, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013411, "present": true, "tradeState": "active", "expires": 1855, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013471, "present": true, "tradeState": "active", "expires": 1795, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013531, "present": true, "tradeState": "active", "expires": 1735, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013591, "present": true, "tradeState": "active", "expires": 1675, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013651, "present": true, "tradeState": "active", "expires": 1615, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013711, "present": true, "tradeState": "active", "expires": 1555, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013771, "present": true, "tradeState": "active", "expires": 1495, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013831, "present": true, "tradeState": "active", "expires": 1435, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013891, "present": true, "tradeState": "active", "expires": 1375, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787013951, "present": true, "tradeState": "active", "expires": 1315, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014011, "present": true, "tradeState": "active", "expires": 1255, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014071, "present": true, "tradeState": "active", "expires": 1195, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014131, "present": true, "tradeState": "active", "expires": 1135, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014191, "present": true, "tradeState": "active", "expires": 1075, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014251, "present": true, "tradeState": "active", "expires": 1015, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014311, "present": true, "tradeState": "active", "expires": 955, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014371, "present": true, "tradeState": "active", "expires": 895, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014431, "present": true, "tradeState": "active", "expires": 835, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014491, "present": true, "tradeState": "active", "expires": 775, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014551, "present": true, "tradeState": "active", "expires": 715, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014611, "present": true, "tradeState": "active", "expires": 655, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014671, "present": true, "tradeState": "active", "expires": 595, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014731, "present": true, "tradeState": "active", "expires": 535, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014791, "present": true, "tradeState": "active", "expires": 475, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014851, "present": true, "tradeState": "active", "expires": 415, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014911, "present": true, "tradeState": "active", "expires": 355, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787014971, "present": true, "tradeState": "active", "expires": 295, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787015031, "present": true, "tradeState": "active", "expires": 235, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787015091, "present": true, "tradeState": "active", "expires": 175, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787015151, "present": true, "tradeState": "active", "expires": 115, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787015211, "present": true, "tradeState": "active", "expires": 55, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787015271, "present": true, "tradeState": "expired", "expires": 0, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||
{"t": 1787015301, "present": false, "tradeState": null, "expires": null, "itemState": null, "startingBid": null, "buyNowPrice": null, "total": 0, "credits": 29843976}
|
||||
@@ -1,91 +0,0 @@
|
||||
{
|
||||
"gap": 2,
|
||||
"test": "expired -> Return to Club -> full exit/re-entry persistence",
|
||||
"date": "2026-08-17",
|
||||
"item": {
|
||||
"wire_item_id": 100000178,
|
||||
"resourceId": 212188,
|
||||
"rating": 75,
|
||||
"preferredPosition": "ST",
|
||||
"tradeId": 1000000178
|
||||
},
|
||||
"natural_expiry": {
|
||||
"method": "read-only sampler polling /tradePile once a minute; expiry is derived from created_at + duration, so it was OBSERVED, never forced. No timestamp was mutated at any point.",
|
||||
"samples": 55,
|
||||
"transitions": [
|
||||
{
|
||||
"t": 1787012091,
|
||||
"tradeState": "active",
|
||||
"expires": 3175,
|
||||
"itemState": "forSale"
|
||||
},
|
||||
{
|
||||
"t": 1787015271,
|
||||
"tradeState": "expired",
|
||||
"expires": 0,
|
||||
"itemState": "forSale",
|
||||
"note": "natural expiry reached; itemState stayed forSale through the transition"
|
||||
},
|
||||
{
|
||||
"t": 1787015301,
|
||||
"tradeState": null,
|
||||
"total": 0,
|
||||
"note": "row left the pile entirely \u2014 the operator's Return to Club"
|
||||
}
|
||||
],
|
||||
"auction_duration_secs": 3600
|
||||
},
|
||||
"return_to_club_server_state": {
|
||||
"tradePile_total": 0,
|
||||
"tradePile_rows": 0,
|
||||
"tradePile_counts": {
|
||||
"count": 0,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 0,
|
||||
"sold": 0
|
||||
},
|
||||
"trade_status": {
|
||||
"tradeId": 1000000178,
|
||||
"tradeState": "closed",
|
||||
"expires": 0
|
||||
},
|
||||
"club_items": 1966,
|
||||
"club_row": {
|
||||
"id": 100000178,
|
||||
"resourceId": 212188,
|
||||
"rating": 75,
|
||||
"preferredPosition": "ST",
|
||||
"itemState": "free",
|
||||
"untradeable": false
|
||||
},
|
||||
"duplicate_owned_ids": 0,
|
||||
"market_store": {
|
||||
"listing_1000000178": "cancelled",
|
||||
"active": 0,
|
||||
"reserved": 0,
|
||||
"all_by_state": {
|
||||
"cancelled": 3
|
||||
}
|
||||
},
|
||||
"coins": 29843976,
|
||||
"fix_a_inactive_rows": 0,
|
||||
"checks_passed": "18/18"
|
||||
},
|
||||
"status": "GAP 2 CLOSED \u2014 Return to Club is durable across repeated full FUT exit/re-entry",
|
||||
"exit_reentry": {
|
||||
"observed": true,
|
||||
"operator_report": "yes that card is still there",
|
||||
"host_log_evidence": [
|
||||
"route=move-items wire=100000178 pile=club auction_cancelled=1",
|
||||
"route=auth-delete",
|
||||
"route=auth ... ip=10.10.0.105 persona=33068179 sid_opened=true (fresh session)",
|
||||
"route=hub clubPlayers=1966 auctionCount=0",
|
||||
"route=club total=1986 emitted=1966 dropped_no_asset=0",
|
||||
"the full auth -> userMassInfo -> hub -> club -> tradePile -> trade/status cycle repeated for a SECOND fresh session"
|
||||
],
|
||||
"session_boundaries_crossed": 2,
|
||||
"server_checks_after_reentry": "18/18, identical to before the re-entry"
|
||||
},
|
||||
"code_change_required": false
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"auctionCount": 1,
|
||||
"clubPlayers": 1965,
|
||||
"tradePile": {
|
||||
"count": 1,
|
||||
"selling": 1,
|
||||
"sold": 0
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"date": "2026-08-17",
|
||||
"change": "removed tradeState:'inactive' projection from /tradePile and /trade/status",
|
||||
"why": "RE of the FUT front-end: the action gate is in the APT/ActionScript layer; only rows classified as auctions (getCardsInAuction/isInActiveAuction) reach PreCheckCardOptions -> handleTradeCardAction, so an 'inactive' row renders and can never be acted on. The corpus already said 'do not emit it' (plan-2026-08-06-transfer-market.md:731-733).",
|
||||
"before": {
|
||||
"total": 7,
|
||||
"by_state": {
|
||||
"expired": 1,
|
||||
"inactive": 6
|
||||
},
|
||||
"credits": 29843976,
|
||||
"trade_status_returned": 7
|
||||
},
|
||||
"after": {
|
||||
"total": 1,
|
||||
"by_state": {
|
||||
"expired": 1
|
||||
},
|
||||
"credits": 29843976,
|
||||
"trade_status_returned": 1
|
||||
},
|
||||
"club_invariant": {
|
||||
"formerly_inactive_members_visible_in_club": 6,
|
||||
"of": 6,
|
||||
"stranded": 0,
|
||||
"club_items": 1965
|
||||
},
|
||||
"not_fixed_here": {
|
||||
"itemState": "listFS",
|
||||
"finding": "0 occurrences in CardsDLL (md5 4de3493131d7d2ff7f8b360c5ac9b655) and 0 in 4.26 GiB of process memory; decodes to -1. FIFA 17 value is forSale (5).",
|
||||
"reason_deferred": "CARD_OFFERSTATE is one of three unresolved action-gate candidates; every actionable row observed carried itemState -1, so changing it could perturb the working expired -> Return-to-Club flow. Ships alone with its own live A/B."
|
||||
},
|
||||
"gate_status": "Category C, STRONGLY SUPPORTED. Gate term NOT PROVEN: STATE, CARD_OFFERSTATE and RESERVEDPRICE/MAX_CREDITS all differ together in every observable row. Proof needs AVM1 disassembly of tradepile.isInActiveAuction."
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
{
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 15000,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 2674,
|
||||
"itemData": {
|
||||
"assetId": 158023,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 96
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 26
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 61
|
||||
}
|
||||
],
|
||||
"cardassetid": 158023,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 158023,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000155,
|
||||
"itemState": "listFS",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 52,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "RW",
|
||||
"rareflag": 1,
|
||||
"rating": 93,
|
||||
"resourceId": 158023,
|
||||
"teamid": 241,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 1000000155,
|
||||
"tradeState": "active",
|
||||
"watched": false
|
||||
},
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 0,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 158023,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 96
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 26
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 61
|
||||
}
|
||||
],
|
||||
"cardassetid": 158023,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 158023,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000059,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 52,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "RW",
|
||||
"rareflag": 1,
|
||||
"rating": 93,
|
||||
"resourceId": 158023,
|
||||
"teamid": 241,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 0,
|
||||
"tradeId": 1000000059,
|
||||
"tradeState": "inactive",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 29843976,
|
||||
"duplicateItemIdList": [],
|
||||
"total": 2
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
{
|
||||
"date": "2026-08-17",
|
||||
"change": "itemData.itemState on the seller's own auction rows: listFS -> forSale",
|
||||
"sites": [
|
||||
"openfut-utas-host/src/market.rs handle_market_query (GET /tradePile)",
|
||||
"openfut-utas-host/src/market.rs handle_market_status (GET /trade/status)"
|
||||
],
|
||||
"why": {
|
||||
"listFS": "not a FIFA 17 token: 0 occurrences in CardsDLL_Win64_retail.dll (md5 4de3493131d7d2ff7f8b360c5ac9b655), 0 in 4.26 GiB of live client memory, decodes to -1 via FUN_180166660",
|
||||
"forSale": "FIFA 17 native, value 5, from the 12-row itemState table at 0x180229cc0",
|
||||
"oracle": "the Python oracle emits listFS (utas_server.py:3921); the differential asserted PARITY and so passed while both sides were wrong"
|
||||
},
|
||||
"supporting_evidence_the_token_is_safe": "the market SEARCH route (/transfermarket) already emitted forSale before this change, so the client has been decoding forSale=5 on a live path all along",
|
||||
"wire_ab": {
|
||||
"baseline": {
|
||||
"tradeId": 1000000155,
|
||||
"tradeState": "expired",
|
||||
"bidState": "none",
|
||||
"expires": 0,
|
||||
"startingBid": 150,
|
||||
"buyNowPrice": 15000,
|
||||
"currentBid": 0,
|
||||
"sellerName": "CAGE",
|
||||
"sellerEstablished": 1,
|
||||
"watched": false,
|
||||
"coinsProcessed": 0,
|
||||
"atom_count": 12,
|
||||
"itemState": "listFS",
|
||||
"item_id": 100000155,
|
||||
"resourceId": 158023,
|
||||
"untradeable": false,
|
||||
"rating": 93,
|
||||
"preferredPosition": "RW"
|
||||
},
|
||||
"candidate": {
|
||||
"tradeId": 1000000155,
|
||||
"tradeState": "expired",
|
||||
"bidState": "none",
|
||||
"expires": 0,
|
||||
"startingBid": 150,
|
||||
"buyNowPrice": 15000,
|
||||
"currentBid": 0,
|
||||
"sellerName": "CAGE",
|
||||
"sellerEstablished": 1,
|
||||
"watched": false,
|
||||
"coinsProcessed": 0,
|
||||
"atom_count": 12,
|
||||
"itemState": "forSale",
|
||||
"item_id": 100000155,
|
||||
"resourceId": 158023,
|
||||
"untradeable": false,
|
||||
"rating": 93,
|
||||
"preferredPosition": "RW"
|
||||
},
|
||||
"fields_changed": [
|
||||
"itemState"
|
||||
],
|
||||
"fields_identical": 17,
|
||||
"credits_before": 29843976,
|
||||
"credits_after": 29843976,
|
||||
"fix_a_inactive_rows_before": 0,
|
||||
"fix_a_inactive_rows_after": 0
|
||||
},
|
||||
"offline_gates": {
|
||||
"host_tests": "126 pass",
|
||||
"adapter_tests": "214 pass",
|
||||
"fmt": "clean",
|
||||
"clippy": "clean",
|
||||
"differential": "relabelled market-query tradePile PARITY -> DIFFERENT-BY-DESIGN; now pins oracle=listFS and rust=forSale so the divergence stays visible"
|
||||
},
|
||||
"status": "PROMOTED \u2014 live-confirmed on the expired path",
|
||||
"live_status": {
|
||||
"session": "operator FIFA 17 session, 2026-08-17, against the already-expired row 1000000155 (res 158023, 93 RW)",
|
||||
"expired_row_actionable": true,
|
||||
"return_to_club_offered": true,
|
||||
"return_to_club_succeeded": true,
|
||||
"operator_report": "the card is back in my club",
|
||||
"server_verified_after": {
|
||||
"tradePile_total": 0,
|
||||
"tradePile_rows": 0,
|
||||
"tradePile_counts": {
|
||||
"count": 0,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 0,
|
||||
"sold": 0
|
||||
},
|
||||
"trade_status_1000000155": "closed",
|
||||
"market_store_state": "cancelled",
|
||||
"club_items": 1966,
|
||||
"club_items_before": 1965,
|
||||
"card_in_club": {
|
||||
"id": 100000155,
|
||||
"resourceId": 158023,
|
||||
"rating": 93,
|
||||
"preferredPosition": "RW",
|
||||
"itemState": "free",
|
||||
"untradeable": false
|
||||
},
|
||||
"duplicate_ids_in_club": 0,
|
||||
"stale_active_listings": 0,
|
||||
"inactive_rows": 0
|
||||
},
|
||||
"active_row_render_with_forSale": "NOT OBSERVED \u2014 no active listing existed during the session",
|
||||
"full_exit_reentry_ui": "NOT DIRECTLY OBSERVED \u2014 durability established from the server state a fresh session reads (empty tradePile + card in club)"
|
||||
},
|
||||
"revert": "single-line: set both auction_record_as(l, \"forSale\") calls in market.rs back to \"listFS\", rebuild, hub restart prod-host",
|
||||
"claim_boundary": "This validates PROTOCOL CORRECTNESS of the token only. It does NOT prove the Flash action-gate term; STATE / CARD_OFFERSTATE / RESERVEDPRICE+MAX_CREDITS remain confounded and the gate stays Category C / STRONGLY SUPPORTED / not proven.",
|
||||
"narrows": "CARD_OFFERSTATE is not a gate term requiring -1: an expired row decoding 5 stayed actionable. STATE and RESERVEDPRICE/MAX_CREDITS remain confounded; the Flash gate is still Category C / not proven."
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
[
|
||||
{
|
||||
"defId": 20801,
|
||||
"maxPrice": 15000,
|
||||
"minPrice": 150
|
||||
}
|
||||
]
|
||||
@@ -1,434 +0,0 @@
|
||||
{
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 15000,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 1474,
|
||||
"itemData": {
|
||||
"assetId": 158023,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 96
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 26
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 61
|
||||
}
|
||||
],
|
||||
"cardassetid": 158023,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 158023,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000155,
|
||||
"itemState": "listFS",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 52,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "RW",
|
||||
"rareflag": 1,
|
||||
"rating": 93,
|
||||
"resourceId": 158023,
|
||||
"teamid": 241,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 1000000155,
|
||||
"tradeState": "active",
|
||||
"watched": false
|
||||
},
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 0,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 172610,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 69
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 33
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 47
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 57
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 80
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 76
|
||||
}
|
||||
],
|
||||
"cardassetid": 172610,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 172610,
|
||||
"discardValue": 900,
|
||||
"fitness": 99,
|
||||
"id": 100004867,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 31,
|
||||
"nation": 54,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CB",
|
||||
"rareflag": 1,
|
||||
"rating": 80,
|
||||
"resourceId": 172610,
|
||||
"teamid": 54,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 0,
|
||||
"tradeId": 1000004867,
|
||||
"tradeState": "inactive",
|
||||
"watched": false
|
||||
},
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 0,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 163824,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 43
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 56
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 62
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 82
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 82
|
||||
}
|
||||
],
|
||||
"cardassetid": 163824,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 163824,
|
||||
"discardValue": 900,
|
||||
"fitness": 99,
|
||||
"id": 100004866,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 13,
|
||||
"nation": 50,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CB",
|
||||
"rareflag": 1,
|
||||
"rating": 83,
|
||||
"resourceId": 163824,
|
||||
"teamid": 7,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 0,
|
||||
"tradeId": 1000004866,
|
||||
"tradeState": "inactive",
|
||||
"watched": false
|
||||
},
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 0,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 177003,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 74
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 75
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 72
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 66
|
||||
}
|
||||
],
|
||||
"cardassetid": 177003,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 177003,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000122,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 10,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CM",
|
||||
"rareflag": 1,
|
||||
"rating": 89,
|
||||
"resourceId": 177003,
|
||||
"teamid": 243,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 0,
|
||||
"tradeId": 1000000122,
|
||||
"tradeState": "inactive",
|
||||
"watched": false
|
||||
},
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 0,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 177003,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 74
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 75
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 72
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 66
|
||||
}
|
||||
],
|
||||
"cardassetid": 177003,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 177003,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000127,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 10,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CM",
|
||||
"rareflag": 1,
|
||||
"rating": 89,
|
||||
"resourceId": 177003,
|
||||
"teamid": 243,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 0,
|
||||
"tradeId": 1000000127,
|
||||
"tradeState": "inactive",
|
||||
"watched": false
|
||||
},
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 0,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 158023,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 96
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 26
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 61
|
||||
}
|
||||
],
|
||||
"cardassetid": 158023,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 158023,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000059,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 52,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "RW",
|
||||
"rareflag": 1,
|
||||
"rating": 93,
|
||||
"resourceId": 158023,
|
||||
"teamid": 241,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 0,
|
||||
"tradeId": 1000000059,
|
||||
"tradeState": "inactive",
|
||||
"watched": false
|
||||
},
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 0,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 158023,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 96
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 26
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 61
|
||||
}
|
||||
],
|
||||
"cardassetid": 158023,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 158023,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000079,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 52,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "RW",
|
||||
"rareflag": 1,
|
||||
"rating": 93,
|
||||
"resourceId": 158023,
|
||||
"teamid": 241,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 0,
|
||||
"tradeId": 1000000079,
|
||||
"tradeState": "inactive",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 29843976,
|
||||
"duplicateItemIdList": [],
|
||||
"total": 7
|
||||
}
|
||||
-190
@@ -1,190 +0,0 @@
|
||||
{
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 15000,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 20801,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 81
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 91
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 33
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"cardassetid": 20801,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 20801,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000097,
|
||||
"itemState": "listFS",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 38,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LW",
|
||||
"rareflag": 1,
|
||||
"rating": 94,
|
||||
"resourceId": 20801,
|
||||
"teamid": 243,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 1000000097,
|
||||
"tradeState": "expired",
|
||||
"watched": false
|
||||
},
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 15000,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 2996,
|
||||
"itemData": {
|
||||
"assetId": 158023,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 96
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 26
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 61
|
||||
}
|
||||
],
|
||||
"cardassetid": 158023,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 158023,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000155,
|
||||
"itemState": "listFS",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 52,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "RW",
|
||||
"rareflag": 1,
|
||||
"rating": 93,
|
||||
"resourceId": 158023,
|
||||
"teamid": 241,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 1000000155,
|
||||
"tradeState": "active",
|
||||
"watched": false
|
||||
},
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 0,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 158023,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 96
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 26
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 61
|
||||
}
|
||||
],
|
||||
"cardassetid": 158023,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 158023,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000059,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 52,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "RW",
|
||||
"rareflag": 1,
|
||||
"rating": 93,
|
||||
"resourceId": 158023,
|
||||
"teamid": 241,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 0,
|
||||
"tradeId": 1000000059,
|
||||
"tradeState": "inactive",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 29843976,
|
||||
"duplicateItemIdList": [],
|
||||
"total": 3
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 15000,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 2220,
|
||||
"itemData": {
|
||||
"assetId": 20801,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 81
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 91
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 33
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"cardassetid": 20801,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 20801,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000097,
|
||||
"itemState": "listFS",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 38,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LW",
|
||||
"rareflag": 1,
|
||||
"rating": 94,
|
||||
"resourceId": 20801,
|
||||
"teamid": 243,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 1000000097,
|
||||
"tradeState": "active",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 29843976
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
{
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 15000,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 1199,
|
||||
"itemData": {
|
||||
"assetId": 20801,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 81
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 91
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 33
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"cardassetid": 20801,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 20801,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000097,
|
||||
"itemState": "listFS",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 38,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LW",
|
||||
"rareflag": 1,
|
||||
"rating": 94,
|
||||
"resourceId": 20801,
|
||||
"teamid": 243,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 1000000097,
|
||||
"tradeState": "active",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 29843976
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"count": 1,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 1,
|
||||
"sold": 0
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
{
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "none",
|
||||
"buyNowPrice": 15000,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 0,
|
||||
"expires": 1199,
|
||||
"itemData": {
|
||||
"assetId": 20801,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 81
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 91
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 33
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"cardassetid": 20801,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 20801,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000097,
|
||||
"itemState": "listFS",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 38,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LW",
|
||||
"rareflag": 1,
|
||||
"rating": 94,
|
||||
"resourceId": 20801,
|
||||
"teamid": 243,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 1000000097,
|
||||
"tradeState": "active",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 29843976,
|
||||
"duplicateItemIdList": [],
|
||||
"total": 1
|
||||
}
|
||||
@@ -1,242 +0,0 @@
|
||||
{
|
||||
"100000059": {
|
||||
"assetId": 158023,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 96
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 26
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 61
|
||||
}
|
||||
],
|
||||
"cardassetid": 158023,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 158023,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000059,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 52,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "RW",
|
||||
"rareflag": 1,
|
||||
"rating": 93,
|
||||
"resourceId": 158023,
|
||||
"teamid": 241,
|
||||
"untradeable": false
|
||||
},
|
||||
"100000122": {
|
||||
"assetId": 177003,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 74
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 75
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 72
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 66
|
||||
}
|
||||
],
|
||||
"cardassetid": 177003,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 177003,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000122,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 10,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CM",
|
||||
"rareflag": 1,
|
||||
"rating": 89,
|
||||
"resourceId": 177003,
|
||||
"teamid": 243,
|
||||
"untradeable": false
|
||||
},
|
||||
"100000127": {
|
||||
"assetId": 177003,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 74
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 75
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 72
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 66
|
||||
}
|
||||
],
|
||||
"cardassetid": 177003,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 177003,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000127,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 10,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CM",
|
||||
"rareflag": 1,
|
||||
"rating": 89,
|
||||
"resourceId": 177003,
|
||||
"teamid": 243,
|
||||
"untradeable": false
|
||||
},
|
||||
"100004866": {
|
||||
"assetId": 163824,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 43
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 56
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 62
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 82
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 82
|
||||
}
|
||||
],
|
||||
"cardassetid": 163824,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 163824,
|
||||
"discardValue": 900,
|
||||
"fitness": 99,
|
||||
"id": 100004866,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 13,
|
||||
"nation": 50,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CB",
|
||||
"rareflag": 1,
|
||||
"rating": 83,
|
||||
"resourceId": 163824,
|
||||
"teamid": 7,
|
||||
"untradeable": false
|
||||
},
|
||||
"100004867": {
|
||||
"assetId": 172610,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 69
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 33
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 47
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 57
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 80
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 76
|
||||
}
|
||||
],
|
||||
"cardassetid": 172610,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 172610,
|
||||
"discardValue": 900,
|
||||
"fitness": 99,
|
||||
"id": 100004867,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 31,
|
||||
"nation": 54,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CB",
|
||||
"rareflag": 1,
|
||||
"rating": 80,
|
||||
"resourceId": 172610,
|
||||
"teamid": 54,
|
||||
"untradeable": false
|
||||
}
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
==============================================================================
|
||||
== auctionInfo deserializer 0x18013e410 — full decompilation
|
||||
==============================================================================
|
||||
|
||||
/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
|
||||
|
||||
undefined4 FUN_18013e410(undefined8 param_1,longlong param_2,undefined8 param_3)
|
||||
|
||||
{
|
||||
longlong *plVar1;
|
||||
undefined1 *puVar2;
|
||||
undefined1 *puVar3;
|
||||
undefined ***pppuVar4;
|
||||
undefined ***pppuVar5;
|
||||
char cVar6;
|
||||
int iVar7;
|
||||
int iVar8;
|
||||
undefined8 uVar9;
|
||||
longlong *plVar10;
|
||||
undefined4 uVar11;
|
||||
undefined1 auStack_138 [32];
|
||||
undefined **local_118;
|
||||
undefined8 local_110;
|
||||
longlong local_108;
|
||||
undefined8 local_100;
|
||||
undefined **local_f8;
|
||||
undefined1 local_f0 [32];
|
||||
undefined1 local_d0;
|
||||
longlong *local_c8;
|
||||
undefined8 local_c0;
|
||||
undefined1 local_b0 [36];
|
||||
undefined4 local_8c;
|
||||
undefined4 local_88;
|
||||
undefined4 local_84;
|
||||
undefined4 local_80;
|
||||
int local_70;
|
||||
undefined8 local_68;
|
||||
undefined1 local_58 [8];
|
||||
longlong local_50;
|
||||
longlong *local_48;
|
||||
int local_40;
|
||||
undefined1 local_3c;
|
||||
undefined1 local_3b;
|
||||
undefined1 local_3a;
|
||||
undefined1 local_39;
|
||||
ulonglong local_38;
|
||||
|
||||
local_100 = 0xfffffffffffffffe;
|
||||
local_38 = DAT_1802db6e0 ^ (ulonglong)auStack_138;
|
||||
iVar8 = 0x38c;
|
||||
FUN_18010f5f0(&local_f8);
|
||||
iVar7 = FUN_1801c7f10(param_2);
|
||||
while (iVar7 != 10) {
|
||||
if (*(int *)(param_2 + 0xd0) == 0xb) {
|
||||
iVar8 = FUN_180180d00(*(undefined8 *)(param_2 + 0xf8));
|
||||
iVar7 = FUN_1801c7f10(param_2);
|
||||
if (iVar7 != 6) goto LAB_18013e498;
|
||||
}
|
||||
else {
|
||||
LAB_18013e498:
|
||||
if (iVar8 < 0x2b8) {
|
||||
if (iVar8 == 0x2b7) {
|
||||
uVar9 = FUN_1801c7aa0(param_2);
|
||||
FUN_180008120(local_b0,uVar9,0x1e);
|
||||
}
|
||||
else if (iVar8 < 0x117) {
|
||||
if (iVar8 == 0x116) {
|
||||
local_68 = FUN_1801c79d0(param_2);
|
||||
}
|
||||
else if (iVar8 == 0x57) {
|
||||
uVar9 = FUN_1801c7aa0(param_2);
|
||||
local_40 = FUN_180166380(uVar9);
|
||||
}
|
||||
else if (iVar8 == 0x65) {
|
||||
uVar9 = FUN_1801c79d0(param_2);
|
||||
local_88 = FUN_1800d7b30(uVar9);
|
||||
}
|
||||
else {
|
||||
if (iVar8 != 0xc1) goto LAB_18013e59b;
|
||||
uVar9 = FUN_1801c79d0(param_2);
|
||||
local_80 = FUN_1800d7b30(uVar9);
|
||||
}
|
||||
}
|
||||
else if (iVar8 == 0x16b) {
|
||||
FUN_18013fe00(local_58,param_2,0);
|
||||
}
|
||||
else if (iVar8 == 0x2b6) {
|
||||
uVar9 = FUN_1801c79d0(param_2);
|
||||
local_8c = FUN_1800d7b30(uVar9);
|
||||
}
|
||||
else {
|
||||
LAB_18013e59b:
|
||||
FUN_180135ff0(iVar8,param_2);
|
||||
}
|
||||
}
|
||||
else if (iVar8 == 0x2e6) {
|
||||
uVar9 = FUN_1801c79d0(param_2);
|
||||
local_84 = FUN_1800d7b30(uVar9);
|
||||
}
|
||||
else if (iVar8 == 0x2f4) {
|
||||
uVar9 = FUN_1801c79d0(param_2);
|
||||
local_39 = FUN_1800d7b50(uVar9);
|
||||
}
|
||||
else if (iVar8 == 0x331) {
|
||||
local_c0 = FUN_1801c79d0(param_2);
|
||||
}
|
||||
else if (iVar8 == 0x335) {
|
||||
uVar9 = FUN_1801c7aa0(param_2);
|
||||
local_70 = FUN_180166bd0(uVar9);
|
||||
}
|
||||
else {
|
||||
if (iVar8 != 0x380) goto LAB_18013e59b;
|
||||
cVar6 = FUN_1801c7620(param_2);
|
||||
local_3c = cVar6 != '\0';
|
||||
}
|
||||
}
|
||||
iVar7 = FUN_1801c7f10(param_2);
|
||||
}
|
||||
if (local_70 == 4) {
|
||||
local_3b = local_40 != 0;
|
||||
}
|
||||
else {
|
||||
local_3b = (local_40 - 1U & 0xfffffffd) == 0;
|
||||
}
|
||||
local_3a = local_40 - 2U < 2;
|
||||
local_d0 = 1;
|
||||
FUN_180007f60(local_f0,param_3,1);
|
||||
plVar10 = (longlong *)FUN_18011a830();
|
||||
if (local_48 == (longlong *)0x0) {
|
||||
local_110 = 0;
|
||||
local_118 = &PTR_LAB_180214de8;
|
||||
local_108 = 0;
|
||||
(**(code **)(*plVar10 + 0xa00))(plVar10,&local_118,local_c0);
|
||||
if (local_108 == 0) {
|
||||
uVar11 = 1;
|
||||
goto LAB_18013e795;
|
||||
}
|
||||
if ((local_58 != (undefined1 *)(local_108 + 0xa0)) &&
|
||||
(plVar1 = *(longlong **)(local_108 + 0xb0), local_48 != plVar1)) {
|
||||
if (local_48 != (longlong *)0x0) {
|
||||
puVar2 = (undefined1 *)*local_48;
|
||||
puVar3 = *(undefined1 **)(puVar2 + 8);
|
||||
if (local_58 == puVar2) {
|
||||
*local_48 = (longlong)puVar3;
|
||||
}
|
||||
else {
|
||||
for (; puVar3 != (undefined1 *)0x0; puVar3 = *(undefined1 **)(puVar3 + 8)) {
|
||||
if (puVar3 == local_58) {
|
||||
*(undefined8 *)(puVar2 + 8) = *(undefined8 *)(puVar3 + 8);
|
||||
break;
|
||||
}
|
||||
puVar2 = puVar3;
|
||||
}
|
||||
}
|
||||
}
|
||||
local_48 = plVar1;
|
||||
if (plVar1 != (longlong *)0x0) {
|
||||
local_50 = *plVar1;
|
||||
*plVar1 = (longlong)local_58;
|
||||
}
|
||||
}
|
||||
local_118 = &PTR_LAB_180214de8;
|
||||
if (local_108 != 0) {
|
||||
pppuVar4 = *(undefined ****)(local_108 + 0x30);
|
||||
pppuVar5 = (undefined ***)pppuVar4[1];
|
||||
if (&local_118 == pppuVar4) {
|
||||
*(undefined ****)(local_108 + 0x30) = pppuVar5;
|
||||
}
|
||||
else {
|
||||
for (; pppuVar5 != (undefined ***)0x0; pppuVar5 = (undefined ***)pppuVar5[1]) {
|
||||
if (pppuVar5 == &local_118) {
|
||||
pppuVar4[1] = pppuVar5[1];
|
||||
break;
|
||||
}
|
||||
pppuVar4 = pppuVar5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(**(code **)(*plVar10 + 0x9f8))(plVar10,param_1,&local_f8);
|
||||
uVar11 = 10;
|
||||
LAB_18013e795:
|
||||
FUN_18000a3c0(local_58);
|
||||
local_f8 = &PTR_FUN_18021b520;
|
||||
while (local_c8 != (longlong *)0x0) {
|
||||
plVar10 = (longlong *)local_c8[1];
|
||||
(**(code **)(*local_c8 + 8))();
|
||||
local_c8 = plVar10;
|
||||
}
|
||||
return uVar11;
|
||||
}
|
||||
|
||||
|
||||
==============================================================================
|
||||
== Flash publisher 0x1801bf030 — full decompilation
|
||||
==============================================================================
|
||||
|
||||
void FUN_1801bf030(longlong *param_1,longlong param_2,undefined8 param_3,undefined8 param_4)
|
||||
|
||||
{
|
||||
longlong lVar1;
|
||||
longlong *plVar2;
|
||||
longlong lVar3;
|
||||
int iVar4;
|
||||
undefined4 local_res8 [2];
|
||||
undefined4 local_res10 [2];
|
||||
undefined4 local_res18 [2];
|
||||
undefined4 local_res20 [2];
|
||||
undefined1 *local_b0;
|
||||
undefined1 *local_a8;
|
||||
undefined1 *local_a0;
|
||||
longlong *local_98;
|
||||
undefined4 local_90;
|
||||
char *local_88;
|
||||
longlong local_80 [2];
|
||||
longlong local_70;
|
||||
longlong *local_68;
|
||||
|
||||
(**(code **)(*param_1 + 8))
|
||||
(param_1,(*(longlong *)(param_2 + 0x38) - *(longlong *)(param_2 + 0x30)) / 0x18,*param_1
|
||||
,param_4,0xfffffffffffffffe);
|
||||
iVar4 = 0;
|
||||
lVar3 = *(longlong *)(param_2 + 0x30);
|
||||
if (lVar3 != *(longlong *)(param_2 + 0x38)) {
|
||||
do {
|
||||
lVar1 = *(longlong *)(lVar3 + 0x10);
|
||||
FUN_1800d8fa0(*(undefined8 *)(lVar1 + 0x38),local_res10,local_res8);
|
||||
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"TRADEID_LOWER",local_res8[0]);
|
||||
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"TRADEID_UPPER",local_res10[0]);
|
||||
FUN_1801a9200(local_80,*(undefined4 *)(lVar1 + 0x6c));
|
||||
local_98 = (longlong *)FUN_1800d7370();
|
||||
local_90 = 1;
|
||||
local_88 = "FUT String";
|
||||
local_b0 = &DAT_1802e13a0;
|
||||
local_a8 = &DAT_1802e13a0;
|
||||
local_a0 = &DAT_1802e13a1;
|
||||
if (*(longlong *)(lVar1 + 0x90) - 1U < 0x20f5800) {
|
||||
FUN_1801c2e50(&local_b0);
|
||||
}
|
||||
else {
|
||||
plVar2 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar2 + 400))(plVar2,&local_b0,"FUT_AUCTION_EXPIRED",1);
|
||||
}
|
||||
(**(code **)(*param_1 + 0x30))(param_1,iVar4,"DURATION",local_b0);
|
||||
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"TIME_REMAINING",*(undefined4 *)(lVar1 + 0x90));
|
||||
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"MIN_CREDITS",*(undefined4 *)(lVar1 + 0x78));
|
||||
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"MAX_CREDITS",*(undefined4 *)(lVar1 + 0x70));
|
||||
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"RESERVEDPRICE",*(undefined4 *)(lVar1 + 0x74));
|
||||
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"YOURBID",*(undefined4 *)(lVar1 + 0xb8));
|
||||
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"STATE",*(undefined4 *)(lVar1 + 0x88));
|
||||
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"COINS_AWARDED",*(undefined1 *)(lVar1 + 0xbf));
|
||||
FUN_1800d8fa0(*(undefined8 *)(*(longlong *)(lVar1 + 0xb0) + 8),local_res18,local_res20);
|
||||
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"UUID_UPPER",local_res18[0]);
|
||||
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"UUID_LOWER",local_res20[0]);
|
||||
(**(code **)(*param_1 + 0x20))
|
||||
(param_1,iVar4,"CARD_ID",*(undefined4 *)(*(longlong *)(lVar1 + 0xb0) + 0x18));
|
||||
(**(code **)(*param_1 + 0x20))
|
||||
(param_1,iVar4,"FIFA_ID",*(uint *)(*(longlong *)(lVar1 + 0xb0) + 0x18) & 0xffffff);
|
||||
(**(code **)(*param_1 + 0x20))
|
||||
(param_1,iVar4,"CARD_TYPE",*(undefined4 *)(*(longlong *)(lVar1 + 0xb0) + 0x4c));
|
||||
(**(code **)(*param_1 + 0x20))
|
||||
(param_1,iVar4,"CARD_OFFERSTATE",*(undefined4 *)(*(longlong *)(lVar1 + 0xb0) + 0x5c)
|
||||
);
|
||||
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"IS_WATCHED",*(undefined1 *)(lVar1 + 0xbc));
|
||||
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"INBOX",*(undefined1 *)(lVar1 + 0xbe));
|
||||
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"IS_GLOW",*(undefined1 *)(lVar1 + 0xbd));
|
||||
(**(code **)(*param_1 + 0x18))(param_1,iVar4,"TRADE_DATA_AVAILABLE",1);
|
||||
if (1 < (longlong)local_a0 - (longlong)local_b0) {
|
||||
if (local_b0 != (undefined1 *)0x0) {
|
||||
(**(code **)(*local_98 + 0x18))(local_98,local_b0,(int)local_a0 - (int)local_b0);
|
||||
}
|
||||
}
|
||||
if ((1 < local_70 - local_80[0]) && (local_80[0] != 0)) {
|
||||
(**(code **)(*local_68 + 0x18))(local_68,local_80[0],(int)local_70 - (int)local_80[0]);
|
||||
}
|
||||
lVar3 = lVar3 + 0x18;
|
||||
iVar4 = iVar4 + 1;
|
||||
} while (lVar3 != *(longlong *)(param_2 + 0x38));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
==============================================================================
|
||||
== strings: sold / clear / inbox / glow / collect
|
||||
==============================================================================
|
||||
sold 0x180228bed 'sold'
|
||||
sold 0x18022f524 'sold'
|
||||
Sold 0x1801efaf1 'SoldFromTradePile'
|
||||
Sold 0x1802302cf 'SoldBid'
|
||||
Sold 0x1802302df 'SoldBuyNow'
|
||||
SOLD 0x18020a0c7 'SOLD'
|
||||
clearSold ABSENT
|
||||
ClearSold ABSENT
|
||||
INBOX 0x180215044 'INBOX'
|
||||
inbox ABSENT
|
||||
IS_GLOW 0x1802151a0 'IS_GLOW'
|
||||
glow ABSENT
|
||||
Glow ABSENT
|
||||
collect 0x1802307e0 'collector'
|
||||
Collect 0x1801ec3c1 'CollectedCards'
|
||||
Collect 0x1801ec3f8 'Collection'
|
||||
Collect 0x1801ec422 'Collection'
|
||||
Collect 0x1801ec449 'Collection'
|
||||
unclaimed 0x18022fd68 'unclaimedPrizesChampionEvents'
|
||||
claim 0x1802099d1 'claimedPack'
|
||||
claim 0x1802099e9 'claimedPack2'
|
||||
claim 0x18022fd6a 'claimedPrizesChampionEvents'
|
||||
@@ -1,30 +0,0 @@
|
||||
"""Q1 — what does FIFA17 do with tradeState == closed(4)?
|
||||
|
||||
Targets the ONE native tradeState consumer branch (cmp [rbp-0x38],0x4 at
|
||||
0x18013e619) inside the auctionInfo deserializer 0x18013e410, and the Flash
|
||||
publisher 0x1801bf030.
|
||||
"""
|
||||
|
||||
print("=" * 78)
|
||||
print("== auctionInfo deserializer 0x18013e410 — full decompilation")
|
||||
print("=" * 78)
|
||||
print(dec(0x18013E410))
|
||||
|
||||
print("=" * 78)
|
||||
print("== Flash publisher 0x1801bf030 — full decompilation")
|
||||
print("=" * 78)
|
||||
print(dec(0x1801BF030))
|
||||
|
||||
print("=" * 78)
|
||||
print("== strings: sold / clear / inbox / glow / collect")
|
||||
print("=" * 78)
|
||||
for needle in (b"sold", b"Sold", b"SOLD", b"clearSold", b"ClearSold",
|
||||
b"INBOX", b"inbox", b"IS_GLOW", b"glow", b"Glow",
|
||||
b"collect", b"Collect", b"unclaimed", b"claim"):
|
||||
hits = find_all(needle, blocks=(".rdata", ".data", ".text"))
|
||||
for h in hits[:14]:
|
||||
s = rd_str(h, 90)
|
||||
if s and len(s) > 2:
|
||||
print(f" {needle.decode():12s} 0x{h:x} {s!r}")
|
||||
if not hits:
|
||||
print(f" {needle.decode():12s} ABSENT")
|
||||
@@ -1,114 +0,0 @@
|
||||
==============================================================================
|
||||
== neighbourhood of SoldBid/SoldBuyNow (0x1802302cf) — read the whole table
|
||||
==============================================================================
|
||||
0x180230280 'auctionLostBidRejected'
|
||||
0x180230298 'auctionLostOutbid'
|
||||
0x1802302b0 'auctionLostOutbidSelf'
|
||||
0x1802302c8 'auctionSoldBid'
|
||||
0x1802302d8 'auctionSoldBuyNow'
|
||||
0x1802302f0 'auctionWonBid'
|
||||
0x180230300 'auctionWonBuyNow'
|
||||
0x180230318 'authToken'
|
||||
0x180230328 'available'
|
||||
0x180230338 'awaykit'
|
||||
0x180230340 'awardCount'
|
||||
0x180230350 'awardedPrizes'
|
||||
0x180230360 'awardItemData'
|
||||
0x180230370 'awardMappings'
|
||||
0x180230380 'awardType'
|
||||
0x180230390 'awardSet'
|
||||
0x1802303a0 'awardSetId'
|
||||
0x1802303ac 'awards'
|
||||
0x1802303b8 'awardValue'
|
||||
0x1802303c8 'badgeDBid'
|
||||
0x1802303d4 'badges'
|
||||
0x1802303dc 'balls'
|
||||
0x1802303e8 'BEGINNER'
|
||||
0x1802303f8 'bestBuilderScore'
|
||||
|
||||
==============================================================================
|
||||
== xrefs to each sold-ish string
|
||||
==============================================================================
|
||||
sold@228bed 0 xref(s)
|
||||
sold@22f524 1 xref(s)
|
||||
from 0x1802d3da8 DATA in ? (entry 0x0)
|
||||
SoldFromTradePile 0 xref(s)
|
||||
SoldBid 0 xref(s)
|
||||
SoldBuyNow 0 xref(s)
|
||||
SOLD@20a0c7 0 xref(s)
|
||||
|
||||
==============================================================================
|
||||
== who reads record +0xbf (COINS_AWARDED) / +0xbe (INBOX) / +0xbd (IS_GLOW)
|
||||
== i.e. the deserializer's coinsProcessed + the two computed flags
|
||||
==============================================================================
|
||||
-- decoder for atom 0x2f4 (coinsProcessed): FUN_1800d7b50
|
||||
|
||||
ulonglong FUN_1800d7b50(longlong param_1)
|
||||
|
||||
{
|
||||
ulonglong in_RAX;
|
||||
byte bVar1;
|
||||
|
||||
if (0 < param_1) {
|
||||
bVar1 = 0xff;
|
||||
if (param_1 < 0xff) {
|
||||
bVar1 = (byte)param_1;
|
||||
}
|
||||
return (ulonglong)bVar1;
|
||||
}
|
||||
return in_RAX & 0xffffffffffffff00;
|
||||
}
|
||||
|
||||
|
||||
|
||||
==============================================================================
|
||||
== route table 0x18021df80 — all 45 rows, looking for a clear/sold route
|
||||
==============================================================================
|
||||
[ 0] 0x18021df80 -> 0x18021e308 'ut/%s/auctionhouse'
|
||||
[ 1] 0x18021df88 -> 0x18021e320 'AUCTIONHOUSE'
|
||||
[ 2] 0x18021df90 -> 0x18021e330 'ut/%s/clubUser'
|
||||
[ 3] 0x18021df98 -> 0x18021e340 'CLUB_USER'
|
||||
[ 4] 0x18021dfa0 -> 0x18021e350 'ut/%s/user/list'
|
||||
[ 5] 0x18021dfa8 -> 0x1801fa0b8 'CLUB_INFO'
|
||||
[ 6] 0x18021dfb0 -> 0x18021e360 'ut/%s/club'
|
||||
[ 7] 0x18021dfb8 -> 0x1801f6438 'CLUB'
|
||||
[ 8] 0x18021dfc0 -> 0x18021e370 'ut/%s/defid'
|
||||
[ 9] 0x18021dfc8 -> 0x18021e37c 'DREAM'
|
||||
[10] 0x18021dfd0 -> 0x18021e388 'ut/%s/squad'
|
||||
[11] 0x18021dfd8 -> 0x18021e394 'SQUAD'
|
||||
[12] 0x18021dfe0 -> 0x18021e3a0 'ut/delete/%s/squad'
|
||||
[13] 0x18021dfe8 -> 0x18021e3b8 'DELETE_SQUAD'
|
||||
[14] 0x18021dff0 -> 0x18021e3c8 'ut/%s/leaderboards/options'
|
||||
[15] 0x18021dff8 -> 0x18021e3e8 'LBOPTIONS'
|
||||
[16] 0x18021e000 -> 0x18021e3f8 'ut/%s/leaderboards'
|
||||
[17] 0x18021e008 -> 0x18021e410 'LBDEFAULT'
|
||||
[18] 0x18021e010 -> 0x18021e420 'ut/%s/activeMessage'
|
||||
[19] 0x18021e018 -> 0x18021e438 'PAFPRACTICE'
|
||||
[20] 0x18021e020 -> 0x18021e444 'ut/%s'
|
||||
[21] 0x18021e028 -> 0x18021e44c 'UT'
|
||||
[22] 0x18021e030 -> 0x18021e450 'ut/%s/user'
|
||||
[23] 0x18021e038 -> 0x18021e45c 'USER'
|
||||
[24] 0x18021e040 -> 0x18021e468 'ut/delete/%s/user'
|
||||
[25] 0x18021e048 -> 0x18021e480 'DELETEUSER'
|
||||
[26] 0x18021e050 -> 0x18021e490 'ut/%s/item'
|
||||
[27] 0x18021e058 -> 0x18021e49c 'ITEMS'
|
||||
[28] 0x18021e060 -> 0x18021e4a8 'ut/%s/item/resource'
|
||||
[29] 0x18021e068 -> 0x18021e4c0 'ITEMS_BY_RES'
|
||||
[30] 0x18021e070 -> 0x18021e4d0 'ut/delete/%s/item'
|
||||
[31] 0x18021e078 -> 0x18021e4e8 'DELETEITEMS'
|
||||
[32] 0x18021e080 -> 0x18021e4f8 'ut/%s/match'
|
||||
[33] 0x18021e088 -> 0x1801ecc34 'MATCH'
|
||||
[34] 0x18021e090 -> 0x18021e508 'ut/%s/sbs'
|
||||
[35] 0x18021e098 -> 0x18021e514 'SBC'
|
||||
[36] 0x18021e0a0 -> 0x18021e518 'ut/%s/tournament'
|
||||
[37] 0x18021e0a8 -> 0x18021e530 'TOURNAMENT'
|
||||
[38] 0x18021e0b0 -> 0x18021e540 'ut/%s/tournament/user'
|
||||
[39] 0x18021e0b8 -> 0x18021e558 'TOURNAMENTUSER'
|
||||
[40] 0x18021e0c0 -> 0x18021e568 'ut/delete/%s/tournament/user'
|
||||
[41] 0x18021e0c8 -> 0x18021e588 'TOURNAMENTQUIT'
|
||||
[42] 0x18021e0d0 -> 0x18021e598 'ut/%s/season'
|
||||
[43] 0x18021e0d8 -> 0x180209fc4 'SEASON'
|
||||
[44] 0x18021e0e0 -> 0x18021e5a8 'ut/%s/season/user'
|
||||
[45] 0x18021e0e8 -> 0x18021e5c0 'SEASONUSER'
|
||||
[46] 0x18021e0f0 -> 0x18021e5d0 'ut/%s/season/%%s/user'
|
||||
[47] 0x18021e0f8 -> 0x18021e5e8 'SEASONUSER_ALTER'
|
||||
@@ -1,50 +0,0 @@
|
||||
"""Q2 — the sold vocabulary: SoldBid / SoldBuyNow / SoldFromTradePile / sold,
|
||||
the coinsProcessed -> COINS_AWARDED path, and the route table (clear-sold?).
|
||||
"""
|
||||
|
||||
print("=" * 78)
|
||||
print("== neighbourhood of SoldBid/SoldBuyNow (0x1802302cf) — read the whole table")
|
||||
print("=" * 78)
|
||||
a = 0x180230280
|
||||
while a < 0x180230400:
|
||||
s = rd_str(a, 60)
|
||||
if s and len(s) > 1 and s.isprintable():
|
||||
print(f" 0x{a:x} {s!r}")
|
||||
a += len(s.encode()) + 1
|
||||
else:
|
||||
a += 1
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== xrefs to each sold-ish string")
|
||||
print("=" * 78)
|
||||
for name, sa in (("sold@228bed", 0x180228BED), ("sold@22f524", 0x18022F524),
|
||||
("SoldFromTradePile", 0x1801EFAF1), ("SoldBid", 0x1802302CF),
|
||||
("SoldBuyNow", 0x1802302DF), ("SOLD@20a0c7", 0x18020A0C7)):
|
||||
xs = xrefs_to(sa)
|
||||
print(f" {name:20s} {len(xs)} xref(s)")
|
||||
for x in xs[:8]:
|
||||
print(f" from 0x{x[0]:x} {x[1]} in {x[2]} (entry 0x{x[3]:x})")
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== who reads record +0xbf (COINS_AWARDED) / +0xbe (INBOX) / +0xbd (IS_GLOW)")
|
||||
print("== i.e. the deserializer's coinsProcessed + the two computed flags")
|
||||
print("=" * 78)
|
||||
print("-- decoder for atom 0x2f4 (coinsProcessed): FUN_1800d7b50")
|
||||
print(dec(0x1800D7B50))
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== route table 0x18021df80 — all 45 rows, looking for a clear/sold route")
|
||||
print("=" * 78)
|
||||
for i in range(48):
|
||||
ea = 0x18021DF80 + i * 8
|
||||
try:
|
||||
p = qword(ea)
|
||||
except Exception:
|
||||
break
|
||||
s = ""
|
||||
if 0x180000000 < p < 0x180400000:
|
||||
s = rd_str(p, 120)
|
||||
print(f" [{i:2d}] 0x{ea:x} -> 0x{p:x} {s!r}")
|
||||
@@ -1,123 +0,0 @@
|
||||
==============================================================================
|
||||
== route table continued from 0x18021e0f8 until it stops looking like one
|
||||
==============================================================================
|
||||
0x18021e100 -> 0x18021e600 'ut/%s/season/%%s/reset'
|
||||
0x18021e108 -> 0x18021e618 'SEASONRESET'
|
||||
0x18021e110 -> 0x18021e628 'ut/%s/season/friendly'
|
||||
0x18021e118 -> 0x18021e640 'FRIENDLYSEASON'
|
||||
0x18021e120 -> 0x18021e650 'ut/%s/purchased'
|
||||
0x18021e128 -> 0x18021e660 'PURCHASED'
|
||||
0x18021e130 -> 0x18021e670 'ut/%s/store'
|
||||
0x18021e138 -> 0x18021e67c 'STORE'
|
||||
0x18021e140 -> 0x18021e688 'ut/%s/watchList'
|
||||
0x18021e148 -> 0x18021e698 'WATCHLIST'
|
||||
0x18021e150 -> 0x18021e6a8 'ut/delete/%s/watchList'
|
||||
0x18021e158 -> 0x18021e6c0 'DELETEWATCHLIST'
|
||||
0x18021e160 -> 0x18021e6d0 'ut/%s/tradePile'
|
||||
0x18021e168 -> 0x18021e6e0 'TRADEPILE'
|
||||
0x18021e170 -> 0x18021e6f0 'ut/%s/trade'
|
||||
0x18021e178 -> 0x18021e6fc 'TRADE'
|
||||
0x18021e180 -> 0x18021e708 'ut/delete/%s/trade'
|
||||
0x18021e188 -> 0x18021e720 'DELETETRADE'
|
||||
0x18021e190 -> 0x18021e730 'ut/%s/marketdata'
|
||||
0x18021e198 -> 0x18021e748 'MARKETDATA'
|
||||
0x18021e1a0 -> 0x18021e758 'ut/%s/clientdata'
|
||||
0x18021e1a8 -> 0x18021e770 'CLIENTDATA'
|
||||
0x18021e1b0 -> 0x18021e780 'ut/auth'
|
||||
0x18021e1b8 -> 0x18021e788 'AUTH'
|
||||
0x18021e1c0 -> 0x18021e790 'ut/delete/auth'
|
||||
0x18021e1c8 -> 0x18021e7a0 'DELETE_AUTH'
|
||||
0x18021e1d0 -> 0x18021e7b0 'ut/%s/phishing'
|
||||
0x18021e1d8 -> 0x18021e7c0 'PHISHING'
|
||||
0x18021e1e0 -> 0x18021e7d0 'ut/%s/captcha'
|
||||
0x18021e1e8 -> 0x18021e7e0 'CAPTCHA'
|
||||
0x18021e1f0 -> 0x18021e7e8 'ut/%s/tfa'
|
||||
0x18021e1f8 -> 0x18021e7f4 'TFA'
|
||||
0x18021e200 -> 0x18021e7f8 'ut/%s/squad/mode'
|
||||
0x18021e208 -> 0x18021e810 'SQUADMODE'
|
||||
0x18021e210 -> 0x18021e820 'ut/%s/draft/mode'
|
||||
0x18021e218 -> 0x18021e834 'DRAFT'
|
||||
0x18021e220 -> 0x18021e840 'ut/%s/champion'
|
||||
0x18021e228 -> 0x18021e850 'CHAMPIONS'
|
||||
0x18021e230 -> 0x18021e860 'ut/v2/%s/store'
|
||||
0x18021e238 -> 0x18021e870 'V2STORE'
|
||||
0x18021e240 -> 0x18021e878 'ut/%s/livemessage'
|
||||
0x18021e248 -> 0x18021e890 'LIVEMESSAGE'
|
||||
0x18021e258 -> 0x18021e89c 'ADMIN'
|
||||
0x18021e268 -> 0x18021e8a4 'DEBUG'
|
||||
0x18021e278 -> 0x18021e8b0 'MAINTENANCE'
|
||||
|
||||
==============================================================================
|
||||
== itemState table 0x180229cc0 (prior work: 12 rows) — dump generously
|
||||
==============================================================================
|
||||
[ 0] 0x180229cc0 str=0x18022a2a0 'invalid' val=0 (0x0)
|
||||
[ 1] 0x180229cd0 str=0x18022a2a8 'free' val=1 (0x1)
|
||||
[ 2] 0x180229ce0 str=0x18022a2b0 'WAITING_FOR_GAME' val=2 (0x2)
|
||||
[ 3] 0x180229cf0 str=0x18022a2c4 'inGame' val=2 (0x2)
|
||||
[ 4] 0x180229d00 str=0x18022a2d0 'forSale' val=5 (0x5)
|
||||
[ 5] 0x180229d10 str=0x18022a2d8 'offered' val=6 (0x6)
|
||||
[ 6] 0x180229d20 str=0x18022a2e0 'activeBadge' val=100 (0x64)
|
||||
[ 7] 0x180229d30 str=0x18022a2f0 'activeHomeKit' val=101 (0x65)
|
||||
[ 8] 0x180229d40 str=0x18022a300 'activeAwayKit' val=102 (0x66)
|
||||
[ 9] 0x180229d50 str=0x18022a310 'activeBall' val=103 (0x67)
|
||||
[10] 0x180229d60 str=0x18022a320 'activeStadium' val=104 (0x68)
|
||||
[11] 0x180229d70 str=0x180221c04 'active' val=255 (0xff)
|
||||
[12] 0x180229d80 str=0x0 '' val=4294967295 (0xffffffff)
|
||||
[13] 0x180229d90 str=0x18022a330 'none' val=8 (0x8)
|
||||
[14] 0x180229da0 str=0x18022a338 'head' val=0 (0x0)
|
||||
[15] 0x180229db0 str=0x18022a340 'shoulder' val=1 (0x1)
|
||||
[16] 0x180229dc0 str=0x18022a34c 'arm' val=2 (0x2)
|
||||
[17] 0x180229dd0 str=0x1802009d0 'back' val=3 (0x3)
|
||||
[18] 0x180229de0 str=0x18022a350 'hip' val=4 (0x4)
|
||||
[19] 0x180229df0 str=0x18022a354 'leg' val=5 (0x5)
|
||||
|
||||
==============================================================================
|
||||
== tradeState table 0x180229e40 — dump generously
|
||||
==============================================================================
|
||||
[ 0] 0x180229e40 str=0x180221c04 'active' val=1 (0x1)
|
||||
[ 1] 0x180229e50 str=0x18022a360 'inactive' val=2 (0x2)
|
||||
[ 2] 0x180229e60 str=0x18022a370 'expired' val=3 (0x3)
|
||||
[ 3] 0x180229e70 str=0x18022a378 'closed' val=4 (0x4)
|
||||
[ 4] 0x180229e80 str=0x0 '' val=0 (0x0)
|
||||
[ 5] 0x180229e90 str=0x4e4f5f53544f4853 '' val=1380013151 (0x5241545f)
|
||||
[ 6] 0x180229ea0 str=0x4653534543435553 '' val=1415531605 (0x545f4c55)
|
||||
[ 7] 0x180229eb0 str=0x5345 '' val=1314017091 (0x4e524f43)
|
||||
[ 8] 0x180229ec0 str=0x4548534e41454c43 '' val=5461061 (0x535445)
|
||||
[ 9] 0x180229ed0 str=0x5f474e4953534150 '' val=1129465168 (0x43524550)
|
||||
[10] 0x180229ee0 str=0x4547 '' val=1397968720 (0x53534f50)
|
||||
[11] 0x180229ef0 str=0x45435245505f4e4f '' val=1195463758 (0x4741544e)
|
||||
|
||||
==============================================================================
|
||||
== pointer arrays containing the sold vocabulary
|
||||
==============================================================================
|
||||
auctionSoldBid pointer found at: ['0x1802d2928']
|
||||
auctionSoldBuyNow pointer found at: ['0x1802d2930']
|
||||
auctionWonBuyNow pointer found at: ['0x1802d2940']
|
||||
sold@22f524 pointer found at: ['0x1802d3da8']
|
||||
sold@228bed pointer found at: NOWHERE
|
||||
SoldFromTradePile pointer found at: NOWHERE
|
||||
SOLD@20a0c7 pointer found at: NOWHERE
|
||||
|
||||
==============================================================================
|
||||
== the .data array around 0x1802d3da8 (the one DATA xref to 'sold')
|
||||
==============================================================================
|
||||
0x1802d3d68 -> 0x1801e98ac 'shots'
|
||||
0x1802d3d70 -> 0x18022f4c8 'shotsOnTarget'
|
||||
0x1802d3d78 -> 0x18022f4d8 'silhouetteName'
|
||||
0x1802d3d80 -> 0x18022f4e8 'silName'
|
||||
0x1802d3d88 -> 0x1801ec108 'silver'
|
||||
0x1802d3d90 -> 0x18022f4f0 'silverQuantity'
|
||||
0x1802d3d98 -> 0x18022f500 'sizeBeforeEncode'
|
||||
0x1802d3da0 -> 0x18022f518 'slotIndex'
|
||||
0x1802d3da8 -> 0x18022f524 'sold' <<<
|
||||
0x1802d3db0 -> 0x18022f52c 'sort'
|
||||
0x1802d3db8 -> 0x18022f538 'sortPriority'
|
||||
0x1802d3dc0 -> 0x18022ed30 'source'
|
||||
0x1802d3dc8 -> 0x18022f548 'squad'
|
||||
0x1802d3dd0 -> 0x18022f550 'squadActives'
|
||||
0x1802d3dd8 -> 0x18022f560 'squadBuildingSetsClientData'
|
||||
0x1802d3de0 -> 0x18022f580 'squadBuildingSetsGracePeriodMinutes'
|
||||
0x1802d3de8 -> 0x18022f5a8 'squadChallenge'
|
||||
0x1802d3df0 -> 0x18022f5b8 'squadId'
|
||||
0x1802d3df8 -> 0x18022f5c0 'squadName'
|
||||
0x1802d3e00 -> 0x18022f5d0 'squadList'
|
||||
@@ -1,68 +0,0 @@
|
||||
"""Q3 — full route table, the itemState/tradeState enum tables, and the pointer
|
||||
arrays that reference the auctionSold* vocabulary and the bare 'sold' string.
|
||||
"""
|
||||
import struct
|
||||
|
||||
print("=" * 78)
|
||||
print("== route table continued from 0x18021e0f8 until it stops looking like one")
|
||||
print("=" * 78)
|
||||
a = 0x18021E100
|
||||
misses = 0
|
||||
while a < 0x18021E400 and misses < 6:
|
||||
p = qword(a)
|
||||
s = rd_str(p, 120) if 0x180000000 < p < 0x180400000 else ""
|
||||
if s and s.isprintable():
|
||||
print(f" 0x{a:x} -> 0x{p:x} {s!r}")
|
||||
misses = 0
|
||||
else:
|
||||
misses += 1
|
||||
a += 8
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== itemState table 0x180229cc0 (prior work: 12 rows) — dump generously")
|
||||
print("=" * 78)
|
||||
for i in range(20):
|
||||
ea = 0x180229CC0 + i * 16
|
||||
p, v = qword(ea), dword(ea + 8)
|
||||
s = rd_str(p, 40) if 0x180000000 < p < 0x180400000 else ""
|
||||
print(f" [{i:2d}] 0x{ea:x} str=0x{p:x} {s!r:24s} val={v} (0x{v:x})")
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== tradeState table 0x180229e40 — dump generously")
|
||||
print("=" * 78)
|
||||
for i in range(12):
|
||||
ea = 0x180229E40 + i * 16
|
||||
p, v = qword(ea), dword(ea + 8)
|
||||
s = rd_str(p, 40) if 0x180000000 < p < 0x180400000 else ""
|
||||
print(f" [{i:2d}] 0x{ea:x} str=0x{p:x} {s!r:24s} val={v} (0x{v:x})")
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== pointer arrays containing the sold vocabulary")
|
||||
print("=" * 78)
|
||||
for label, target in (("auctionSoldBid", 0x1802302C8),
|
||||
("auctionSoldBuyNow", 0x1802302D8),
|
||||
("auctionWonBuyNow", 0x180230300),
|
||||
("sold@22f524", 0x18022F524),
|
||||
("sold@228bed", 0x180228BED),
|
||||
("SoldFromTradePile", 0x1801EFAF1),
|
||||
("SOLD@20a0c7", 0x18020A0C7)):
|
||||
pat = struct.pack("<Q", target)
|
||||
hits = find_all(pat, blocks=(".rdata", ".data", ".text"))
|
||||
print(f" {label:20s} pointer found at: {[hex(h) for h in hits] or 'NOWHERE'}")
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== the .data array around 0x1802d3da8 (the one DATA xref to 'sold')")
|
||||
print("=" * 78)
|
||||
for i in range(-8, 12):
|
||||
ea = 0x1802D3DA8 + i * 8
|
||||
try:
|
||||
p = qword(ea)
|
||||
except Exception:
|
||||
continue
|
||||
s = rd_str(p, 50) if 0x180000000 < p < 0x180400000 else ""
|
||||
mark = " <<<" if i == 0 else ""
|
||||
print(f" 0x{ea:x} -> 0x{p:x} {s!r}{mark}")
|
||||
@@ -1,67 +0,0 @@
|
||||
"""Q4 — is the atom ID the index into the alphabetical atom-name pointer table?
|
||||
|
||||
Validate against the twelve KNOWN auctionInfo atoms. If all twelve agree on one
|
||||
base, the table is the atom dictionary and we can read the ID of any name.
|
||||
"""
|
||||
import struct
|
||||
|
||||
KNOWN = { # atom id -> name, from the confirmed auctionInfo deserializer
|
||||
0x57: "bidState", 0x65: "buyNowPrice", 0xC1: "currentBid", 0x116: "expires",
|
||||
0x16B: "itemData", 0x2B6: "sellerEstablished", 0x2B7: "sellerName",
|
||||
0x2E6: "startingBid", 0x2F4: "coinsProcessed", 0x331: "tradeId",
|
||||
0x335: "tradeState", 0x380: "watched",
|
||||
}
|
||||
|
||||
|
||||
def str_addr(name):
|
||||
"""Address of the exact NUL-terminated string `name`."""
|
||||
for h in find_all(name.encode() + b"\x00", blocks=(".rdata", ".data")):
|
||||
return h
|
||||
return None
|
||||
|
||||
|
||||
def ptr_addr(sa):
|
||||
hits = find_all(struct.pack("<Q", sa), blocks=(".rdata", ".data"))
|
||||
return hits[0] if hits else None
|
||||
|
||||
|
||||
print("=" * 78)
|
||||
print("== solve for the table base using the twelve known atoms")
|
||||
print("=" * 78)
|
||||
bases = {}
|
||||
for aid, name in sorted(KNOWN.items()):
|
||||
sa = str_addr(name)
|
||||
pa = ptr_addr(sa) if sa else None
|
||||
if pa is None:
|
||||
print(f" 0x{aid:03x} {name:20s} string=0x{sa or 0:x} pointer=NOT FOUND")
|
||||
continue
|
||||
base = pa - 8 * aid
|
||||
bases[base] = bases.get(base, 0) + 1
|
||||
print(f" 0x{aid:03x} {name:20s} str=0x{sa:x} ptr=0x{pa:x} -> base 0x{base:x}")
|
||||
|
||||
print()
|
||||
print(" base histogram:", {hex(b): n for b, n in sorted(bases.items(), key=lambda kv: -kv[1])})
|
||||
if not bases:
|
||||
raise SystemExit("no bases resolved")
|
||||
BASE = max(bases, key=bases.get)
|
||||
print(f" CONSENSUS BASE = 0x{BASE:x} ({bases[BASE]}/{len(KNOWN)} atoms agree)")
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== read the atom id of every sold/counts-related name")
|
||||
print("=" * 78)
|
||||
for name in ("sold", "selling", "offered", "count", "maxAuctionsAllowed",
|
||||
"auctionSoldBid", "auctionSoldBuyNow", "auctionWonBid",
|
||||
"auctionWonBuyNow", "auctionLostOutbid", "auctionLostOutbidSelf",
|
||||
"auctionLostBidRejected", "credits", "coins", "tradeIdStr",
|
||||
"itemState", "offers", "bids", "watched", "expires"):
|
||||
sa = str_addr(name)
|
||||
pa = ptr_addr(sa) if sa else None
|
||||
if pa is None:
|
||||
print(f" {name:24s} ABSENT from the table")
|
||||
continue
|
||||
off = pa - BASE
|
||||
if off % 8:
|
||||
print(f" {name:24s} ptr=0x{pa:x} misaligned (off {off})")
|
||||
continue
|
||||
print(f" {name:24s} ptr=0x{pa:x} ATOM ID = 0x{off // 8:x} ({off // 8})")
|
||||
@@ -1,80 +0,0 @@
|
||||
==============================================================================
|
||||
== atom ids, resolved against the table range only
|
||||
==============================================================================
|
||||
sold str=0x18022f524 ptr=0x1802d3da8 ATOM ID = 0x2c9 (713)
|
||||
count str=0x180230abc ptr=0x1802d2d40 ATOM ID = 0xbc (188)
|
||||
offered str=0x18022a2d8 ptr=0x1802d3688 ATOM ID = 0x1e5 (485)
|
||||
selling str=0x18022f460 ptr=0x1802d3d20 ATOM ID = 0x2b8 (696)
|
||||
maxAuctionsAllowed str=0x1802318a8 ptr=0x1802d3558 ATOM ID = 0x1bf (447)
|
||||
credits str=0x180230af8 ptr=0x1802d2d60 ATOM ID = 0xc0 (192)
|
||||
auctionInfo str=0x180230270 ptr=0x1802d2908 ATOM ID = 0x35 (53)
|
||||
itemData str=0x180231438 ptr=0x1802d32b8 ATOM ID = 0x16b (363)
|
||||
bidState str=0x180230458 ptr=0x1802d2a18 ATOM ID = 0x57 (87)
|
||||
tradeState str=0x18022fb48 ptr=0x1802d4108 ATOM ID = 0x335 (821)
|
||||
coinsProcessed str=0x18022f768 ptr=0x1802d3f00 ATOM ID = 0x2f4 (756)
|
||||
offers str=0x180231ae4 ptr=0x1802d3690 ATOM ID = 0x1e6 (486)
|
||||
duplicateItemIdList str=0x180230d18 ptr=0x1802d2ec0 ATOM ID = 0xec (236)
|
||||
total str=0x18022fa4c ptr=0x1802d4088 ATOM ID = 0x325 (805)
|
||||
|
||||
==============================================================================
|
||||
== functions that compare against >=3 of the auction-outcome atoms 0x36..0x3c
|
||||
==============================================================================
|
||||
scanned 13308 functions
|
||||
0x180043b90 FUN_180043b90 sees ['0x36', '0x37', '0x38', '0x3c']
|
||||
0x1800455b0 FUN_1800455b0 sees ['0x37', '0x38', '0x39']
|
||||
0x18006b4c0 FUN_18006b4c0 sees ['0x38', '0x39', '0x3c']
|
||||
0x180094ce0 FUN_180094ce0 sees ['0x36', '0x37', '0x38']
|
||||
0x180097c70 FUN_180097c70 sees ['0x36', '0x37', '0x38']
|
||||
0x1800cabb0 FUN_1800cabb0 sees ['0x38', '0x39', '0x3a']
|
||||
0x1800cf3b0 FUN_1800cf3b0 sees ['0x36', '0x38', '0x3a', '0x3c']
|
||||
0x1800e6410 FUN_1800e6410 sees ['0x36', '0x37', '0x38', '0x39']
|
||||
0x1800ee7c0 FUN_1800ee7c0 sees ['0x36', '0x37', '0x38', '0x39', '0x3a', '0x3c']
|
||||
0x1800fb5b0 FUN_1800fb5b0 sees ['0x38', '0x39', '0x3a', '0x3b']
|
||||
0x1801071f0 FUN_1801071f0 sees ['0x36', '0x37', '0x38', '0x3a', '0x3b', '0x3c']
|
||||
0x18012b370 FUN_18012b370 sees ['0x36', '0x37', '0x38', '0x39', '0x3a', '0x3b', '0x3c']
|
||||
0x18012fd40 FUN_18012fd40 sees ['0x36', '0x37', '0x38', '0x3c']
|
||||
0x18013fe00 FUN_18013fe00 sees ['0x36', '0x38', '0x3a', '0x3b']
|
||||
0x180163770 FUN_180163770 sees ['0x36', '0x38', '0x3a']
|
||||
0x180172d00 FUN_180172d00 sees ['0x38', '0x3a', '0x3c']
|
||||
|
||||
==============================================================================
|
||||
== string neighbourhoods: SoldFromTradePile / SOLD / sold
|
||||
==============================================================================
|
||||
-- SoldFromTradePile
|
||||
0x1801efa40 'CardsRequestStaffBonuses'
|
||||
0x1801efa60 'CardsGetStaffBonuses'
|
||||
0x1801efa78 'WatchListRequest'
|
||||
0x1801efa90 'WatchListResultsRequest'
|
||||
0x1801efaa8 'AddToWatchList'
|
||||
0x1801efab8 'RemoveFromWatchList'
|
||||
0x1801efad0 'RemoveFromTradePile'
|
||||
0x1801efae8 'RemoveAllSoldFromTradePile'
|
||||
0x1801efb08 'GetPhishingQuestion'
|
||||
0x1801efb20 'SetPhishingAnswer'
|
||||
0x1801efb38 'ValidatePhishingAnswer'
|
||||
0x1801efb50 'RetrieveTrustedConsoleList'
|
||||
0x1801efb70 'EnableLiveStreaming'
|
||||
-- SOLD
|
||||
0x18020a040 'D_PRIZES'
|
||||
0x18020a050 'EVENT_ID'
|
||||
0x18020a05c 'TEXT0'
|
||||
0x18020a068 'FUT_UC_SQUAD'
|
||||
0x18020a078 'TEXT1'
|
||||
0x18020a080 'FUT_TOTAL_AUCTIONS'
|
||||
0x18020a098 'FUT_UC_ITEMS'
|
||||
0x18020a0a8 'FUT_TF_SELLING'
|
||||
0x18020a0b8 'TEXT2'
|
||||
0x18020a0c0 'FUT_TF_SOLD'
|
||||
0x18020a0cc 'TEXT3'
|
||||
0x18020a0d8 'FUT_TF_WINNING'
|
||||
0x18020a0e8 'FUT_TF_OUTBID'
|
||||
0x18020a0f8 'FUT_GH_TOTAL_PLAYERS_0'
|
||||
0x18020a110 'FUT_GH_TOTAL_PLAYERS_1'
|
||||
0x18020a128 'FUT_GH_TOP_SCORER'
|
||||
-- sold@228bed
|
||||
0x180228bc8 'RS4:FutISRemoveTradeServerResponse'
|
||||
0x180228bec '/sold'
|
||||
0x180228c00 '\\�&'
|
||||
0x180228c11 "�'"
|
||||
0x180228c28 "X�'"
|
||||
0x180228c38 "`�'"
|
||||
@@ -1,83 +0,0 @@
|
||||
"""Q5 — fix the atom lookup, then find the CONSUMER of atoms 0x36..0x3c
|
||||
(auctionLost*/auctionSold*/auctionWon*) and read the sold-related string
|
||||
neighbourhoods.
|
||||
"""
|
||||
import struct
|
||||
|
||||
BASE = 0x1802D2760
|
||||
TABLE_LO, TABLE_HI = 0x1802D2760, 0x1802D4800
|
||||
|
||||
|
||||
def atom_id(name):
|
||||
"""Every string with this exact spelling, then the pointer that lies INSIDE
|
||||
the atom table. Taking the first string match is wrong: common words appear
|
||||
in several unrelated tables."""
|
||||
out = []
|
||||
for sa in find_all(name.encode() + b"\x00", blocks=(".rdata", ".data")):
|
||||
for pa in find_all(struct.pack("<Q", sa), blocks=(".rdata", ".data")):
|
||||
if TABLE_LO <= pa < TABLE_HI and (pa - BASE) % 8 == 0:
|
||||
out.append((sa, pa, (pa - BASE) // 8))
|
||||
return out
|
||||
|
||||
|
||||
print("=" * 78)
|
||||
print("== atom ids, resolved against the table range only")
|
||||
print("=" * 78)
|
||||
for name in ("sold", "count", "offered", "selling", "maxAuctionsAllowed",
|
||||
"credits", "auctionInfo", "itemData", "bidState", "tradeState",
|
||||
"coinsProcessed", "offers", "duplicateItemIdList", "total"):
|
||||
res = atom_id(name)
|
||||
if not res:
|
||||
print(f" {name:22s} ABSENT from the atom table")
|
||||
for sa, pa, aid in res:
|
||||
print(f" {name:22s} str=0x{sa:x} ptr=0x{pa:x} ATOM ID = 0x{aid:x} ({aid})")
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== functions that compare against >=3 of the auction-outcome atoms 0x36..0x3c")
|
||||
print("=" * 78)
|
||||
WANT = set(range(0x36, 0x3D))
|
||||
hits = {}
|
||||
fi = fm.getFunctions(True)
|
||||
n = 0
|
||||
for f in fi:
|
||||
n += 1
|
||||
ent = int(f.getEntryPoint().getOffset())
|
||||
body = f.getBody()
|
||||
seen = set()
|
||||
it = listing.getInstructions(body, True)
|
||||
while it.hasNext():
|
||||
ins = it.next()
|
||||
m = ins.getMnemonicString()
|
||||
if m not in ("CMP", "SUB", "MOV", "LEA"):
|
||||
continue
|
||||
for i in range(ins.getNumOperands()):
|
||||
for o in ins.getOpObjects(i):
|
||||
try:
|
||||
v = int(o.getValue())
|
||||
except Exception:
|
||||
continue
|
||||
if v in WANT:
|
||||
seen.add(v)
|
||||
if len(seen) >= 3:
|
||||
hits[ent] = (f.getName(), sorted(hex(v) for v in seen))
|
||||
print(f" scanned {n} functions")
|
||||
for ent, (nm, vals) in sorted(hits.items()):
|
||||
print(f" 0x{ent:x} {nm:28s} sees {vals}")
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== string neighbourhoods: SoldFromTradePile / SOLD / sold")
|
||||
print("=" * 78)
|
||||
for label, lo, hi in (("SoldFromTradePile", 0x1801EFA40, 0x1801EFB80),
|
||||
("SOLD", 0x18020A040, 0x18020A140),
|
||||
("sold@228bed", 0x180228B60, 0x180228C60)):
|
||||
print(f"-- {label}")
|
||||
a = lo
|
||||
while a < hi:
|
||||
s = rd_str(a, 70)
|
||||
if s and len(s) > 1 and s.isprintable():
|
||||
print(f" 0x{a:x} {s!r}")
|
||||
a += len(s.encode()) + 1
|
||||
else:
|
||||
a += 1
|
||||
@@ -1,469 +0,0 @@
|
||||
==============================================================================
|
||||
== (a) xrefs: request names + the /sold path suffix
|
||||
==============================================================================
|
||||
RemoveAllSoldFromTradePile 1 xref(s)
|
||||
0x1800293fe DATA in FUN_180028b50 (entry 0x180028b50)
|
||||
RemoveFromTradePile 1 xref(s)
|
||||
0x1800293d4 DATA in FUN_180028b50 (entry 0x180028b50)
|
||||
AddToWatchList 1 xref(s)
|
||||
0x180029380 DATA in FUN_180028b50 (entry 0x180028b50)
|
||||
/sold suffix 1 xref(s)
|
||||
0x180164813 DATA in FUN_1801647c0 (entry 0x1801647c0)
|
||||
RS4:FutISRemoveTradeServerResponse 2 xref(s)
|
||||
0x1801defe9 DATA in Unwind@1801defd0 (entry 0x1801defd0)
|
||||
0x18016488d DATA in FUN_180164860 (entry 0x180164860)
|
||||
|
||||
==============================================================================
|
||||
== (b) xrefs: the Transfer List caption keys
|
||||
==============================================================================
|
||||
FUT_TOTAL_AUCTIONS 1 xref(s)
|
||||
0x1800b1ef8 DATA in FUN_1800b1dc0 (entry 0x1800b1dc0)
|
||||
FUT_UC_ITEMS 4 xref(s)
|
||||
0x1800fbbdc DATA in FUN_1800fbae0 (entry 0x1800fbae0)
|
||||
0x1800fbe5a DATA in FUN_1800fbae0 (entry 0x1800fbae0)
|
||||
0x1800b1fe7 DATA in FUN_1800b1dc0 (entry 0x1800b1dc0)
|
||||
0x1800b21a9 DATA in FUN_1800b1dc0 (entry 0x1800b1dc0)
|
||||
FUT_TF_SELLING 1 xref(s)
|
||||
0x1800b2023 DATA in FUN_1800b1dc0 (entry 0x1800b1dc0)
|
||||
FUT_TF_SOLD 1 xref(s)
|
||||
0x1800b208e DATA in FUN_1800b1dc0 (entry 0x1800b1dc0)
|
||||
FUT_TF_WINNING 1 xref(s)
|
||||
0x1800b21e5 DATA in FUN_1800b1dc0 (entry 0x1800b1dc0)
|
||||
FUT_TF_OUTBID 1 xref(s)
|
||||
0x1800b2250 DATA in FUN_1800b1dc0 (entry 0x1800b1dc0)
|
||||
|
||||
caption publisher candidates (entry -> #caption refs): {'0x1800b1dc0': 7, '0x1800fbae0': 2}
|
||||
|
||||
==============================================================================
|
||||
== decompiled caption publisher 0x1800b1dc0
|
||||
==============================================================================
|
||||
|
||||
void FUN_1800b1dc0(undefined8 param_1,longlong *param_2)
|
||||
|
||||
{
|
||||
char cVar1;
|
||||
undefined8 uVar2;
|
||||
longlong lVar3;
|
||||
longlong *plVar4;
|
||||
ushort uVar5;
|
||||
longlong *local_res10;
|
||||
undefined1 *local_90;
|
||||
undefined1 *local_88;
|
||||
undefined1 *local_80;
|
||||
longlong *local_78;
|
||||
undefined4 local_70;
|
||||
char *local_68;
|
||||
undefined1 *local_60;
|
||||
undefined1 *local_58;
|
||||
undefined1 *local_50;
|
||||
longlong *local_48;
|
||||
undefined4 local_40;
|
||||
char *local_38;
|
||||
|
||||
uVar2 = FUN_1800d7170();
|
||||
FUN_180009c80(&local_res10,uVar2);
|
||||
lVar3 = (**(code **)(*local_res10 + 0x1f8))(local_res10);
|
||||
local_78 = (longlong *)FUN_1800d7370();
|
||||
local_70 = 1;
|
||||
local_68 = "FUT String";
|
||||
local_90 = &DAT_1802e13a0;
|
||||
local_88 = &DAT_1802e13a0;
|
||||
local_80 = &DAT_1802e13a1;
|
||||
local_48 = (longlong *)FUN_1800d7370();
|
||||
local_40 = 1;
|
||||
local_38 = "FUT String";
|
||||
local_60 = &DAT_1802e13a0;
|
||||
local_58 = &DAT_1802e13a0;
|
||||
local_50 = &DAT_1802e13a1;
|
||||
(**(code **)(*param_2 + 0x58))(param_2,4);
|
||||
(**(code **)(*param_2 + 0x78))(param_2,0,"UPDATE_TYPE",PTR_s_PANEL_UPDATE_1802a4fb0);
|
||||
(**(code **)(*param_2 + 0x70))(param_2,0,"TILE_ID",0x1b0);
|
||||
(**(code **)(*param_2 + 0x70))(param_2,0,"ADDON_ENUM",5);
|
||||
plVar4 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar4 + 0xb8))(plVar4,&local_90,*(undefined4 *)(lVar3 + 0x38));
|
||||
(**(code **)(*param_2 + 0x78))(param_2,0,"TEXT0",local_90);
|
||||
plVar4 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar4 + 400))(plVar4,&local_90,"FUT_TOTAL_AUCTIONS",1);
|
||||
(**(code **)(*param_2 + 0x78))(param_2,0,"TEXT1",local_90);
|
||||
(**(code **)(*param_2 + 0x78))(param_2,1,"UPDATE_TYPE",PTR_s_PANEL_UPDATE_1802a4fb0);
|
||||
(**(code **)(*param_2 + 0x70))(param_2,1,"TILE_ID",0x1c0);
|
||||
uVar5 = *(ushort *)(lVar3 + 0x1d6);
|
||||
if (0x62 < uVar5) {
|
||||
uVar5 = 99;
|
||||
}
|
||||
(**(code **)(*param_2 + 0x70))(param_2,1,"NOTIFICATION",uVar5);
|
||||
(**(code **)(*param_2 + 0x70))(param_2,1,"ADDON_ENUM");
|
||||
plVar4 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar4 + 0xb8))(plVar4,&local_90,*(undefined2 *)(lVar3 + 0x1d4));
|
||||
(**(code **)(*param_2 + 0x78))(param_2,1,"TEXT0",local_90);
|
||||
plVar4 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar4 + 400))(plVar4,&local_90,"FUT_UC_ITEMS",1);
|
||||
(**(code **)(*param_2 + 0x78))(param_2,1,"TEXT1",local_90);
|
||||
plVar4 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar4 + 400))(plVar4,&local_90,"FUT_TF_SELLING",1);
|
||||
plVar4 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar4 + 0xb8))(plVar4,&local_60,*(undefined2 *)(lVar3 + 0x1d2));
|
||||
FUN_18000fbd0(&local_90,local_90,local_60);
|
||||
(**(code **)(*param_2 + 0x78))(param_2,1,"TEXT2",local_90);
|
||||
plVar4 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar4 + 400))(plVar4,&local_90,"FUT_TF_SOLD",1);
|
||||
plVar4 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar4 + 0xb8))(plVar4,&local_60,*(undefined2 *)(lVar3 + 0x1d8));
|
||||
FUN_18000fbd0(&local_90,local_90,local_60);
|
||||
(**(code **)(*param_2 + 0x78))(param_2,1,"TEXT3",local_90);
|
||||
(**(code **)(*param_2 + 0x78))(param_2,2,"UPDATE_TYPE",PTR_s_PANEL_UPDATE_1802a4fb0);
|
||||
(**(code **)(*param_2 + 0x70))(param_2,2,"TILE_ID",0x1d0);
|
||||
uVar5 = 99;
|
||||
if (*(ushort *)(lVar3 + 0x1ce) < 99) {
|
||||
uVar5 = *(ushort *)(lVar3 + 0x1ce);
|
||||
}
|
||||
(**(code **)(*param_2 + 0x70))(param_2,2,"NOTIFICATION",uVar5);
|
||||
(**(code **)(*param_2 + 0x70))(param_2,2,"ADDON_ENUM");
|
||||
plVar4 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar4 + 0xb8))(plVar4,&local_90,*(undefined2 *)(lVar3 + 0x1ca));
|
||||
(**(code **)(*param_2 + 0x78))(param_2,2,"TEXT0",local_90);
|
||||
plVar4 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar4 + 400))(plVar4,&local_90,"FUT_UC_ITEMS",1);
|
||||
(**(code **)(*param_2 + 0x78))(param_2,2,"TEXT1",local_90);
|
||||
plVar4 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar4 + 400))(plVar4,&local_90,"FUT_TF_WINNING",1);
|
||||
plVar4 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar4 + 0xb8))(plVar4,&local_60,*(undefined2 *)(lVar3 + 0x1c8));
|
||||
FUN_18000fbd0(&local_90,local_90,local_60);
|
||||
(**(code **)(*param_2 + 0x78))(param_2,2,"TEXT2",local_90);
|
||||
plVar4 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar4 + 400))(plVar4,&local_90,"FUT_TF_OUTBID",1);
|
||||
plVar4 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar4 + 0xb8))(plVar4,&local_60,*(undefined2 *)(lVar3 + 0x1cc));
|
||||
FUN_18000fbd0(&local_90,local_90,local_60);
|
||||
(**(code **)(*param_2 + 0x78))(param_2,2,"TEXT3",local_90);
|
||||
uVar2 = FUN_1800264e0();
|
||||
cVar1 = FUN_180026510(uVar2);
|
||||
if (cVar1 == '\0') {
|
||||
(**(code **)(*param_2 + 0x78))(param_2,3,"UPDATE_TYPE",PTR_s_PANEL_UPDATE_1802a4fb0);
|
||||
(**(code **)(*param_2 + 0x70))(param_2,3,"TILE_ID",0x200);
|
||||
(**(code **)(*param_2 + 0x70))(param_2,3,"ADDON_ENUM");
|
||||
}
|
||||
if ((1 < (longlong)local_50 - (longlong)local_60) && (local_60 != (undefined1 *)0x0)) {
|
||||
(**(code **)(*local_48 + 0x18))(local_48,local_60,(int)local_50 - (int)local_60);
|
||||
}
|
||||
if ((1 < (longlong)local_80 - (longlong)local_90) && (local_90 != (undefined1 *)0x0)) {
|
||||
(**(code **)(*local_78 + 0x18))(local_78,local_90,(int)local_80 - (int)local_90);
|
||||
}
|
||||
(**(code **)(*local_res10 + 8))(local_res10);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
==============================================================================
|
||||
== decompiled caption publisher 0x1800fbae0
|
||||
==============================================================================
|
||||
|
||||
/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
|
||||
/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
|
||||
|
||||
uint FUN_1800fbae0(undefined8 param_1,undefined4 param_2,undefined8 *param_3)
|
||||
|
||||
{
|
||||
undefined1 *puVar1;
|
||||
longlong lVar2;
|
||||
uint uVar3;
|
||||
int iVar4;
|
||||
longlong *plVar5;
|
||||
undefined8 uVar6;
|
||||
longlong *plVar7;
|
||||
longlong lVar8;
|
||||
size_t sVar9;
|
||||
size_t sVar10;
|
||||
undefined1 auStack_2e8 [32];
|
||||
undefined4 local_2c8;
|
||||
undefined1 local_2b8;
|
||||
uint local_2b4;
|
||||
undefined4 local_2b0;
|
||||
undefined4 uStack_2ac;
|
||||
undefined1 *local_2a8;
|
||||
undefined1 *local_2a0;
|
||||
undefined1 *local_298;
|
||||
longlong *local_290;
|
||||
undefined4 local_288;
|
||||
char *local_280;
|
||||
undefined1 *local_278;
|
||||
undefined1 *local_270;
|
||||
undefined1 *local_268;
|
||||
longlong *local_260;
|
||||
undefined4 local_258;
|
||||
char *local_250;
|
||||
undefined1 *local_248;
|
||||
undefined1 *local_240;
|
||||
undefined1 *local_238;
|
||||
longlong *local_230;
|
||||
undefined4 local_228;
|
||||
char *local_220;
|
||||
longlong local_218;
|
||||
undefined8 local_210;
|
||||
longlong local_208;
|
||||
longlong *local_200;
|
||||
undefined4 local_1f8;
|
||||
char *local_1f0;
|
||||
void *local_1e8;
|
||||
longlong local_1e0;
|
||||
longlong local_1d8;
|
||||
longlong *local_1d0;
|
||||
undefined8 local_1b8;
|
||||
longlong *local_1a8;
|
||||
undefined8 local_1a0;
|
||||
undefined8 local_198;
|
||||
undefined8 local_190;
|
||||
undefined4 local_188;
|
||||
undefined2 local_184;
|
||||
undefined8 local_180;
|
||||
undefined8 local_178;
|
||||
undefined8 local_170;
|
||||
undefined8 local_168;
|
||||
undefined2 local_160;
|
||||
undefined4 local_15c;
|
||||
ulonglong local_158;
|
||||
undefined8 uStack_150;
|
||||
undefined4 local_148;
|
||||
undefined2 local_144;
|
||||
undefined4 local_140;
|
||||
undefined **local_138;
|
||||
undefined8 local_130;
|
||||
undefined8 local_128;
|
||||
undefined8 local_120;
|
||||
undefined8 local_118;
|
||||
undefined8 local_110;
|
||||
undefined8 local_108;
|
||||
undefined8 local_100;
|
||||
undefined4 local_f8;
|
||||
undefined2 local_f4;
|
||||
undefined1 local_f2;
|
||||
undefined1 local_f0 [168];
|
||||
ulonglong local_48;
|
||||
|
||||
local_1b8 = 0xfffffffffffffffe;
|
||||
local_48 = DAT_1802db6e0 ^ (ulonglong)auStack_2e8;
|
||||
local_200 = (longlong *)FUN_1800d7370();
|
||||
local_218 = 0;
|
||||
local_208 = 0;
|
||||
local_1f8 = 1;
|
||||
local_1f0 = "FUT Vector";
|
||||
FUN_18004fff0(0,0,local_2b8);
|
||||
local_210 = 0;
|
||||
local_2b0 = 0;
|
||||
uVar3 = FUN_1800fb8c0(param_1,param_2,&local_218,&local_2b0);
|
||||
if ((undefined1 *)*param_3 != (undefined1 *)param_3[1]) {
|
||||
*(undefined1 *)*param_3 = 0;
|
||||
param_3[1] = *param_3;
|
||||
}
|
||||
local_2b4 = uVar3;
|
||||
local_260 = (longlong *)FUN_1800d7370();
|
||||
lVar2 = local_218;
|
||||
local_258 = 1;
|
||||
local_250 = "FUT String";
|
||||
local_278 = &DAT_1802e13a0;
|
||||
local_270 = &DAT_1802e13a0;
|
||||
local_268 = &DAT_1802e13a1;
|
||||
if (uVar3 < 2) {
|
||||
if (uVar3 == 1) {
|
||||
local_1a8 = (longlong *)0x0;
|
||||
local_1a0 = 0;
|
||||
local_198 = 0;
|
||||
local_190 = 0;
|
||||
local_188 = 0;
|
||||
local_184 = 0;
|
||||
local_180 = 0;
|
||||
local_178 = 0;
|
||||
local_170 = 0;
|
||||
local_168 = 0;
|
||||
local_160 = 0x100;
|
||||
lVar8 = -1;
|
||||
local_15c = 0xffffffff;
|
||||
local_158 = _DAT_1801f66a0;
|
||||
uStack_150 = _UNK_1801f66a8;
|
||||
local_148 = 0;
|
||||
local_144 = 0;
|
||||
local_140 = 0;
|
||||
local_130 = 0;
|
||||
local_138 = &PTR_LAB_1801eaac0;
|
||||
local_128 = 0;
|
||||
local_120 = 0;
|
||||
local_118 = 0;
|
||||
local_f4 = 0;
|
||||
local_f2 = 0;
|
||||
local_110 = 0;
|
||||
local_108 = 0;
|
||||
local_100 = 0;
|
||||
local_f8 = 0;
|
||||
memset(local_f0,0,0xa0);
|
||||
local_190 = CONCAT44(local_190._4_4_,*(undefined4 *)(lVar2 + 4)) & 0xffffffff00ffffff;
|
||||
FUN_1801362e0(&local_1a8);
|
||||
FUN_180141660(&local_1a8);
|
||||
uVar6 = FUN_1800d7170();
|
||||
FUN_180009c80(&local_2b0,uVar6);
|
||||
plVar5 = (longlong *)CONCAT44(uStack_2ac,local_2b0);
|
||||
local_2c8 = (undefined4)local_190;
|
||||
(**(code **)(*plVar5 + 0x490))(plVar5,&local_1e8,local_15c,local_158 & 0xffffffff);
|
||||
sVar10 = local_1e0 - (longlong)local_1e8;
|
||||
sVar9 = sVar10;
|
||||
if (0 < (longlong)sVar10) {
|
||||
sVar9 = 0;
|
||||
}
|
||||
iVar4 = memcmp(local_1e8,&DAT_1801e9caf,sVar9);
|
||||
if (((iVar4 == 0) && (-1 < (longlong)sVar10)) && ((longlong)sVar10 < 1)) {
|
||||
local_230 = (longlong *)FUN_1800d7370();
|
||||
local_228 = 1;
|
||||
local_220 = "FUT String";
|
||||
local_248 = &DAT_1802e13a0;
|
||||
local_240 = &DAT_1802e13a0;
|
||||
local_238 = &DAT_1802e13a1;
|
||||
local_290 = (longlong *)FUN_1800d7370();
|
||||
local_288 = 1;
|
||||
local_280 = "FUT String";
|
||||
local_2a8 = &DAT_1802e13a0;
|
||||
local_2a0 = &DAT_1802e13a0;
|
||||
local_298 = &DAT_1802e13a1;
|
||||
FUN_18000fbd0(&local_248,"AWARD_LABEL_%i",*(undefined4 *)(lVar2 + 8));
|
||||
plVar7 = (longlong *)FUN_18019e320();
|
||||
puVar1 = local_248;
|
||||
(**(code **)(*plVar7 + 400))(plVar7,&local_2a8,local_248,1);
|
||||
sVar10 = (longlong)local_2a0 - (longlong)local_2a8;
|
||||
sVar9 = sVar10;
|
||||
if (0 < (longlong)sVar10) {
|
||||
sVar9 = 0;
|
||||
}
|
||||
iVar4 = memcmp(local_2a8,&DAT_1801e9caf,sVar9);
|
||||
if (((iVar4 == 0) && (-1 < (longlong)sVar10)) && ((longlong)sVar10 < 1)) {
|
||||
plVar7 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar7 + 400))(plVar7,&local_2a8,"FUT_UC_ITEMS",1);
|
||||
}
|
||||
FUN_18000fbd0(param_3,&DAT_18021940c,*(undefined4 *)(lVar2 + 0xc));
|
||||
do {
|
||||
lVar8 = lVar8 + 1;
|
||||
} while (local_2a8[lVar8] != '\0');
|
||||
FUN_18000f650(param_3,local_2a8,local_2a8 + lVar8);
|
||||
if (1 < (longlong)local_298 - (longlong)local_2a8) {
|
||||
if (local_2a8 != (undefined1 *)0x0) {
|
||||
(**(code **)(*local_290 + 0x18))(local_290,local_2a8,(int)local_298 - (int)local_2a8);
|
||||
}
|
||||
}
|
||||
if (1 < (longlong)local_238 - (longlong)puVar1) {
|
||||
if (puVar1 != (undefined1 *)0x0) {
|
||||
(**(code **)(*local_230 + 0x18))(local_230,puVar1,(int)local_238 - (int)local_248);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
FUN_18000fbd0(param_3,&DAT_18021940c,*(undefined4 *)(lVar2 + 0xc));
|
||||
do {
|
||||
lVar8 = lVar8 + 1;
|
||||
} while (*(char *)((longlong)local_1e8 + lVar8) != '\0');
|
||||
FUN_18000f650(param_3,local_1e8,(longlong)local_1e8 + lVar8);
|
||||
}
|
||||
if ((1 < local_1d8 - (longlong)local_1e8) && (local_1e8 != (void *)0x0)) {
|
||||
(**(code **)(*local_1d0 + 0x18))(local_1d0,local_1e8,(int)local_1d8 - (int)local_1e8);
|
||||
}
|
||||
(**(code **)(*plVar5 + 8))(plVar5);
|
||||
FUN_18000a3c0(&local_138);
|
||||
plVar5 = local_1a8;
|
||||
while (uVar3 = local_2b4, plVar5 != (longlong *)0x0) {
|
||||
plVar7 = (longlong *)plVar5[1];
|
||||
(**(code **)(*plVar5 + 8))();
|
||||
plVar5 = plVar7;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
plVar5 = (longlong *)FUN_18019e320();
|
||||
(**(code **)(*plVar5 + 400))(plVar5,&local_278,"FUT_UC_ITEMS",1);
|
||||
FUN_18000fbd0(param_3,"%i %s",local_2b0,local_278);
|
||||
}
|
||||
if (1 < (longlong)local_268 - (longlong)local_278) {
|
||||
if (local_278 != (undefined1 *)0x0) {
|
||||
(**(code **)(*local_260 + 0x18))(local_260,local_278,(int)local_268 - (int)local_278);
|
||||
}
|
||||
}
|
||||
FUN_18004fff0(lVar2,local_210,local_2b8);
|
||||
if (lVar2 != 0) {
|
||||
(**(code **)(*local_200 + 0x18))(local_200,lVar2,((local_208 - lVar2) / 0x38) * 0x38);
|
||||
}
|
||||
return uVar3;
|
||||
}
|
||||
|
||||
|
||||
|
||||
==============================================================================
|
||||
== (c) GetAuctionCount deserializer FUN_180163770
|
||||
==============================================================================
|
||||
|
||||
/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
|
||||
|
||||
undefined8 FUN_180163770(undefined8 param_1,undefined8 *param_2)
|
||||
|
||||
{
|
||||
undefined2 uVar1;
|
||||
int iVar2;
|
||||
undefined4 uVar3;
|
||||
undefined8 uVar4;
|
||||
longlong *plVar5;
|
||||
longlong lVar6;
|
||||
undefined1 auStack_168 [32];
|
||||
int local_148 [2];
|
||||
undefined8 local_140;
|
||||
undefined1 local_138 [288];
|
||||
ulonglong local_18;
|
||||
|
||||
local_140 = 0xfffffffffffffffe;
|
||||
local_18 = DAT_1802db6e0 ^ (ulonglong)auStack_168;
|
||||
FUN_1801c63e0(local_138,0,0);
|
||||
local_148[0] = 0x38c;
|
||||
uVar4 = FUN_180008130(*param_2);
|
||||
FUN_1801c8270(local_138,*param_2,uVar4,0);
|
||||
FUN_1801c7f10(local_138);
|
||||
FUN_1801c7f10(local_138);
|
||||
plVar5 = (longlong *)FUN_18011a830();
|
||||
lVar6 = (**(code **)(*plVar5 + 0x130))(plVar5);
|
||||
*(undefined4 *)(lVar6 + 0x30) = 0xffffffff;
|
||||
*(undefined8 *)(lVar6 + 0x34) = 0;
|
||||
iVar2 = FUN_1801c7f10(local_138);
|
||||
while (iVar2 != 10) {
|
||||
iVar2 = FUN_180141ee0(local_148,local_138);
|
||||
if (iVar2 != 6) {
|
||||
if (local_148[0] == 0xbc) {
|
||||
uVar4 = FUN_1801c79d0(local_138);
|
||||
uVar1 = FUN_1800d7b10(uVar4);
|
||||
*(undefined2 *)(lVar6 + 0x34) = uVar1;
|
||||
}
|
||||
else if (local_148[0] == 0x1bf) {
|
||||
uVar4 = FUN_1801c79d0(local_138);
|
||||
uVar3 = FUN_1800d7af0(uVar4);
|
||||
*(undefined4 *)(lVar6 + 0x30) = uVar3;
|
||||
}
|
||||
else if (local_148[0] == 0x1e5) {
|
||||
uVar4 = FUN_1801c79d0(local_138);
|
||||
uVar1 = FUN_1800d7b10(uVar4);
|
||||
*(undefined2 *)(lVar6 + 0x3a) = uVar1;
|
||||
}
|
||||
else if (local_148[0] == 0x2b8) {
|
||||
uVar4 = FUN_1801c79d0(local_138);
|
||||
uVar1 = FUN_1800d7b10(uVar4);
|
||||
*(undefined2 *)(lVar6 + 0x36) = uVar1;
|
||||
}
|
||||
else if (local_148[0] == 0x2c9) {
|
||||
uVar4 = FUN_1801c79d0(local_138);
|
||||
uVar1 = FUN_1800d7b10(uVar4);
|
||||
*(undefined2 *)(lVar6 + 0x38) = uVar1;
|
||||
}
|
||||
else {
|
||||
FUN_180135ff0(local_148[0],local_138);
|
||||
}
|
||||
}
|
||||
iVar2 = FUN_1801c7f10(local_138);
|
||||
}
|
||||
*(undefined1 *)(lVar6 + 0x28) = 1;
|
||||
FUN_1801c6560(local_138);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
"""Q6 — the decisive reads:
|
||||
(a) who builds the /sold request (clear-all-sold) and what it is called;
|
||||
(b) who publishes FUT_TF_SELLING / FUT_TF_SOLD / FUT_TF_WINNING / FUT_TF_OUTBID,
|
||||
i.e. which count feeds the seller's SOLD bucket;
|
||||
(c) the GetAuctionCount deserializer, to pin the five counts atoms.
|
||||
"""
|
||||
|
||||
print("=" * 78)
|
||||
print("== (a) xrefs: request names + the /sold path suffix")
|
||||
print("=" * 78)
|
||||
for label, a in (("RemoveAllSoldFromTradePile", 0x1801EFAE8),
|
||||
("RemoveFromTradePile", 0x1801EFAD0),
|
||||
("AddToWatchList", 0x1801EFAA8),
|
||||
("/sold suffix", 0x180228BEC),
|
||||
("RS4:FutISRemoveTradeServerResponse", 0x180228BC8)):
|
||||
xs = xrefs_to(a)
|
||||
print(f" {label:36s} {len(xs)} xref(s)")
|
||||
for x in xs[:10]:
|
||||
print(f" 0x{x[0]:x} {x[1]:12s} in {x[2]} (entry 0x{x[3]:x})")
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== (b) xrefs: the Transfer List caption keys")
|
||||
print("=" * 78)
|
||||
caps = (("FUT_TOTAL_AUCTIONS", 0x18020A080), ("FUT_UC_ITEMS", 0x18020A098),
|
||||
("FUT_TF_SELLING", 0x18020A0A8), ("FUT_TF_SOLD", 0x18020A0C0),
|
||||
("FUT_TF_WINNING", 0x18020A0D8), ("FUT_TF_OUTBID", 0x18020A0E8))
|
||||
owners = {}
|
||||
for label, a in caps:
|
||||
xs = xrefs_to(a)
|
||||
print(f" {label:20s} {len(xs)} xref(s)")
|
||||
for x in xs[:10]:
|
||||
print(f" 0x{x[0]:x} {x[1]:12s} in {x[2]} (entry 0x{x[3]:x})")
|
||||
owners[x[3]] = owners.get(x[3], 0) + 1
|
||||
|
||||
print()
|
||||
print(" caption publisher candidates (entry -> #caption refs):",
|
||||
{hex(k): v for k, v in sorted(owners.items(), key=lambda kv: -kv[1])})
|
||||
|
||||
for ent in sorted(owners, key=lambda e: -owners[e])[:2]:
|
||||
print()
|
||||
print("=" * 78)
|
||||
print(f"== decompiled caption publisher 0x{ent:x}")
|
||||
print("=" * 78)
|
||||
print(dec(ent))
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== (c) GetAuctionCount deserializer FUN_180163770")
|
||||
print("=" * 78)
|
||||
print(dec(0x180163770))
|
||||
@@ -1,178 +0,0 @@
|
||||
==============================================================================
|
||||
== (a) hub tradePile sub-deserializer 0x18013ead0
|
||||
== atom ids of interest: count=0xbc notification=0x1da selling=0x2b8 sold=0x2c9
|
||||
==============================================================================
|
||||
|
||||
undefined4 FUN_18013ead0(longlong param_1,longlong param_2,longlong param_3)
|
||||
|
||||
{
|
||||
undefined2 uVar1;
|
||||
int iVar2;
|
||||
int iVar3;
|
||||
undefined8 uVar4;
|
||||
|
||||
iVar3 = 0x38c;
|
||||
iVar2 = FUN_1801c7f10(param_3);
|
||||
do {
|
||||
if (iVar2 == 10) {
|
||||
*(undefined1 *)(param_1 + 0x1da) = 1;
|
||||
return 10;
|
||||
}
|
||||
if (*(int *)(param_3 + 0xd0) == 0xb) {
|
||||
iVar3 = FUN_180180d00(*(undefined8 *)(param_3 + 0xf8));
|
||||
iVar2 = FUN_1801c7f10(param_3);
|
||||
if (iVar2 != 6) goto LAB_18013eb2e;
|
||||
}
|
||||
else {
|
||||
LAB_18013eb2e:
|
||||
if (iVar3 == 0xbc) {
|
||||
uVar4 = FUN_1801c79d0(param_3);
|
||||
uVar1 = FUN_1800d7b10(uVar4);
|
||||
*(undefined2 *)(param_1 + 0x1d4) = uVar1;
|
||||
}
|
||||
else if (iVar3 == 0x1da) {
|
||||
uVar4 = FUN_1801c79d0(param_3);
|
||||
uVar1 = FUN_1800d7b10(uVar4);
|
||||
*(undefined2 *)(param_1 + 0x1d6) = uVar1;
|
||||
}
|
||||
else if (iVar3 == 0x2b8) {
|
||||
uVar4 = FUN_1801c79d0(param_3);
|
||||
uVar1 = FUN_1800d7b10(uVar4);
|
||||
*(undefined2 *)(param_1 + 0x1d2) = uVar1;
|
||||
uVar4 = FUN_1801c79d0(param_3);
|
||||
uVar1 = FUN_1800d7b10(uVar4);
|
||||
*(undefined2 *)(param_2 + 0x36) = uVar1;
|
||||
}
|
||||
else if (iVar3 == 0x2c9) {
|
||||
uVar4 = FUN_1801c79d0(param_3);
|
||||
uVar1 = FUN_1800d7b10(uVar4);
|
||||
*(undefined2 *)(param_1 + 0x1d8) = uVar1;
|
||||
}
|
||||
else {
|
||||
FUN_180135ff0(iVar3,param_3);
|
||||
}
|
||||
}
|
||||
iVar2 = FUN_1801c7f10(param_3);
|
||||
} while( true );
|
||||
}
|
||||
|
||||
|
||||
|
||||
==============================================================================
|
||||
== (b) /sold request builder 0x1801647c0
|
||||
==============================================================================
|
||||
|
||||
/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
|
||||
|
||||
void FUN_1801647c0(longlong param_1,undefined8 param_2,undefined4 param_3)
|
||||
|
||||
{
|
||||
undefined1 auStack_58 [32];
|
||||
undefined1 local_38;
|
||||
undefined8 local_37;
|
||||
undefined8 local_2f;
|
||||
undefined8 local_27;
|
||||
undefined4 local_1f;
|
||||
undefined2 local_1b;
|
||||
undefined1 local_19;
|
||||
ulonglong local_18;
|
||||
|
||||
local_18 = DAT_1802db6e0 ^ (ulonglong)auStack_58;
|
||||
local_38 = 0;
|
||||
local_37 = 0;
|
||||
local_2f = 0;
|
||||
local_27 = 0;
|
||||
local_1f = 0;
|
||||
local_1b = 0;
|
||||
local_19 = 0;
|
||||
if (*(longlong *)(param_1 + 0x10) == 0) {
|
||||
FUN_180007f80(&local_38,0x20,"/sold");
|
||||
}
|
||||
else {
|
||||
FUN_180007f80(&local_38,0x20,"/%lld");
|
||||
}
|
||||
FUN_180008100(param_2,&local_38,param_3);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
==============================================================================
|
||||
== (c) FutGetAuctionCount deserializer 0x180163770
|
||||
==============================================================================
|
||||
|
||||
/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
|
||||
|
||||
undefined8 FUN_180163770(undefined8 param_1,undefined8 *param_2)
|
||||
|
||||
{
|
||||
undefined2 uVar1;
|
||||
int iVar2;
|
||||
undefined4 uVar3;
|
||||
undefined8 uVar4;
|
||||
longlong *plVar5;
|
||||
longlong lVar6;
|
||||
undefined1 auStack_168 [32];
|
||||
int local_148 [2];
|
||||
undefined8 local_140;
|
||||
undefined1 local_138 [288];
|
||||
ulonglong local_18;
|
||||
|
||||
local_140 = 0xfffffffffffffffe;
|
||||
local_18 = DAT_1802db6e0 ^ (ulonglong)auStack_168;
|
||||
FUN_1801c63e0(local_138,0,0);
|
||||
local_148[0] = 0x38c;
|
||||
uVar4 = FUN_180008130(*param_2);
|
||||
FUN_1801c8270(local_138,*param_2,uVar4,0);
|
||||
FUN_1801c7f10(local_138);
|
||||
FUN_1801c7f10(local_138);
|
||||
plVar5 = (longlong *)FUN_18011a830();
|
||||
lVar6 = (**(code **)(*plVar5 + 0x130))(plVar5);
|
||||
*(undefined4 *)(lVar6 + 0x30) = 0xffffffff;
|
||||
*(undefined8 *)(lVar6 + 0x34) = 0;
|
||||
iVar2 = FUN_1801c7f10(local_138);
|
||||
while (iVar2 != 10) {
|
||||
iVar2 = FUN_180141ee0(local_148,local_138);
|
||||
if (iVar2 != 6) {
|
||||
if (local_148[0] == 0xbc) {
|
||||
uVar4 = FUN_1801c79d0(local_138);
|
||||
uVar1 = FUN_1800d7b10(uVar4);
|
||||
*(undefined2 *)(lVar6 + 0x34) = uVar1;
|
||||
}
|
||||
else if (local_148[0] == 0x1bf) {
|
||||
uVar4 = FUN_1801c79d0(local_138);
|
||||
uVar3 = FUN_1800d7af0(uVar4);
|
||||
*(undefined4 *)(lVar6 + 0x30) = uVar3;
|
||||
}
|
||||
else if (local_148[0] == 0x1e5) {
|
||||
uVar4 = FUN_1801c79d0(local_138);
|
||||
uVar1 = FUN_1800d7b10(uVar4);
|
||||
*(undefined2 *)(lVar6 + 0x3a) = uVar1;
|
||||
}
|
||||
else if (local_148[0] == 0x2b8) {
|
||||
uVar4 = FUN_1801c79d0(local_138);
|
||||
uVar1 = FUN_1800d7b10(uVar4);
|
||||
*(undefined2 *)(lVar6 + 0x36) = uVar1;
|
||||
}
|
||||
else if (local_148[0] == 0x2c9) {
|
||||
uVar4 = FUN_1801c79d0(local_138);
|
||||
uVar1 = FUN_1800d7b10(uVar4);
|
||||
*(undefined2 *)(lVar6 + 0x38) = uVar1;
|
||||
}
|
||||
else {
|
||||
FUN_180135ff0(local_148[0],local_138);
|
||||
}
|
||||
}
|
||||
iVar2 = FUN_1801c7f10(local_138);
|
||||
}
|
||||
*(undefined1 *)(lVar6 + 0x28) = 1;
|
||||
FUN_1801c6560(local_138);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
==============================================================================
|
||||
== callers of the hub tradePile sub-deserializer (which response embeds it)
|
||||
==============================================================================
|
||||
0x180139610 FUN_180139610
|
||||
@@ -1,31 +0,0 @@
|
||||
"""Q7 — close it:
|
||||
(a) hub tradePile sub-deserializer 0x18013ead0: which ATOM writes +0x1d8 (the
|
||||
offset the FUT_TF_SOLD caption reads)?
|
||||
(b) the /sold request builder 0x1801647c0 — bulk clear, or per-trade?
|
||||
(c) FutGetAuctionCount deserializer 0x180163770 — the five counts atoms.
|
||||
"""
|
||||
|
||||
print("=" * 78)
|
||||
print("== (a) hub tradePile sub-deserializer 0x18013ead0")
|
||||
print("== atom ids of interest: count=0xbc notification=0x1da selling=0x2b8 sold=0x2c9")
|
||||
print("=" * 78)
|
||||
print(dec(0x18013EAD0))
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== (b) /sold request builder 0x1801647c0")
|
||||
print("=" * 78)
|
||||
print(dec(0x1801647C0))
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== (c) FutGetAuctionCount deserializer 0x180163770")
|
||||
print("=" * 78)
|
||||
print(dec(0x180163770))
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== callers of the hub tradePile sub-deserializer (which response embeds it)")
|
||||
print("=" * 78)
|
||||
for a, n in callers(0x18013EAD0):
|
||||
print(f" 0x{a:x} {n}")
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Q8 — are atoms 0x36..0x3c (auctionLost*/auctionSold*/auctionWon*) real response
|
||||
keys? If a deserializer dispatches on them, the server tells the client HOW an
|
||||
auction concluded via its own atom, and bidState is not the carrier.
|
||||
|
||||
Signature of a genuine atom dispatcher: the SAME local compared against many atom
|
||||
ids, with FUN_180135ff0 (value-SKIP) as the else, and FUN_1801c7f10 driving the loop.
|
||||
"""
|
||||
|
||||
CANDIDATES = (0x18012B370, 0x1801071F0, 0x1800EE7C0, 0x1800FB5B0, 0x1800CF3B0)
|
||||
|
||||
for ent in CANDIDATES:
|
||||
print("=" * 78)
|
||||
print(f"== 0x{ent:x} callees include value-SKIP? "
|
||||
f"{any(c[0] == 0x180135FF0 for c in callees(ent))}"
|
||||
f" atom-loop? {any(c[0] == 0x1801C7F10 for c in callees(ent))}")
|
||||
print("=" * 78)
|
||||
body = dec(ent)
|
||||
# Only print if it smells like an atom dispatcher, else just report.
|
||||
if "FUN_180135ff0" in body or "FUN_1801c7f10" in body:
|
||||
print(body[:6000])
|
||||
else:
|
||||
print(" NOT an atom dispatcher (no value-SKIP, no atom loop) — small-immediate noise")
|
||||
print(f" size {len(body)} chars; first 400:")
|
||||
print(body[:400])
|
||||
print()
|
||||
@@ -1,62 +0,0 @@
|
||||
==============================================================================
|
||||
== localisation / string keys about tax, fee, net proceeds
|
||||
==============================================================================
|
||||
TAX (no printable hits)
|
||||
tax (no printable hits)
|
||||
Tax (no printable hits)
|
||||
FEE 0x18023a3a8 'FEET_ATTEMPTED'
|
||||
FEE 0x18023a3c0 'FEET_COMPLETED'
|
||||
_FEE 0x18023a3a7 '_FEET_ATTEMPTED'
|
||||
_FEE 0x18023a3bf '_FEET_COMPLETED'
|
||||
fee (no printable hits)
|
||||
NET 0x180209b1c 'NETOURNAMENT_0'
|
||||
NET 0x180209b3c 'NETOURNAMENT_1'
|
||||
NET 0x18020dd10 'NET'
|
||||
_NET 0x18020dd0f '_NET'
|
||||
RECEIVE 0x18023933f 'RECEIVER'
|
||||
RECEIVE 0x180239350 'RECEIVER'
|
||||
RECEIVE 0x18023983f 'RECEIVER'
|
||||
RECEIVE 0x18023990d 'RECEIVER'
|
||||
RECEIVE 0x180239a0b 'RECEIVER'
|
||||
RECEIVE 0x18023a06f 'RECEIVED'
|
||||
Receive (no printable hits)
|
||||
PROCEEDS (no printable hits)
|
||||
COMMISSION (no printable hits)
|
||||
EA_TAX (no printable hits)
|
||||
FUT_TF_ 0x18020a0a8 'FUT_TF_SELLING'
|
||||
FUT_TF_ 0x18020a0c0 'FUT_TF_SOLD'
|
||||
FUT_TF_ 0x18020a0d8 'FUT_TF_WINNING'
|
||||
FUT_TF_ 0x18020a0e8 'FUT_TF_OUTBID'
|
||||
AFTER_TAX (no printable hits)
|
||||
earnings (no printable hits)
|
||||
Earnings (no printable hits)
|
||||
|
||||
==============================================================================
|
||||
== float constants 0.95 / 0.05 / 0.95f / 0.05f
|
||||
==============================================================================
|
||||
double 0.95 ABSENT
|
||||
double 0.05 ABSENT
|
||||
float 0.95f ABSENT
|
||||
float 0.05f ABSENT
|
||||
|
||||
==============================================================================
|
||||
== functions using BOTH 95 (or 5) and 100 as immediates — integer fee math
|
||||
==============================================================================
|
||||
17 candidate(s)
|
||||
0x18000b5c0 FUN_18000b5c0 immediates [5, 100]
|
||||
0x180051cd0 FUN_180051cd0 immediates [5, 20, 100]
|
||||
0x180057b00 FUN_180057b00 immediates [5, 100]
|
||||
0x180082c30 FUN_180082c30 immediates [5, 100]
|
||||
0x180088cb0 FUN_180088cb0 immediates [5, 100]
|
||||
0x1800a3cb0 FUN_1800a3cb0 immediates [5, 100]
|
||||
0x1800a47b0 FUN_1800a47b0 immediates [5, 100]
|
||||
0x1800d5050 FUN_1800d5050 immediates [5, 20, 100]
|
||||
0x1800d5450 FUN_1800d5450 immediates [5, 20, 100]
|
||||
0x1801129f0 FUN_1801129f0 immediates [5, 100]
|
||||
0x18013af30 FUN_18013af30 immediates [5, 20, 100]
|
||||
0x18013ec10 FUN_18013ec10 immediates [5, 100]
|
||||
0x18013fe00 FUN_18013fe00 immediates [5, 100]
|
||||
0x180147070 FUN_180147070 immediates [5, 100]
|
||||
0x180180ea0 FUN_180180ea0 immediates [5, 100]
|
||||
0x1801adae0 FUN_1801adae0 immediates [5, 100]
|
||||
0x1801b9e60 FUN_1801b9e60 immediates [5, 100]
|
||||
@@ -1,69 +0,0 @@
|
||||
"""Task B — does FIFA17.exe itself compute the transfer fee / net proceeds?
|
||||
|
||||
If the list-item confirmation screen shows "you will receive N", the rounding rule
|
||||
is IN THE BINARY and needs no live measurement. Look for:
|
||||
* localisation keys mentioning tax/fee/net/receive
|
||||
* float constants 0.95 / 0.05
|
||||
* integer 95 or 5 used with 100 in the same function
|
||||
"""
|
||||
import struct
|
||||
|
||||
print("=" * 78)
|
||||
print("== localisation / string keys about tax, fee, net proceeds")
|
||||
print("=" * 78)
|
||||
for needle in (b"TAX", b"tax", b"Tax", b"FEE", b"_FEE", b"fee", b"NET", b"_NET",
|
||||
b"RECEIVE", b"Receive", b"PROCEEDS", b"COMMISSION", b"EA_TAX",
|
||||
b"FUT_TF_", b"AFTER_TAX", b"earnings", b"Earnings"):
|
||||
hits = find_all(needle, blocks=(".rdata", ".data"))
|
||||
shown = 0
|
||||
for h in hits:
|
||||
s = rd_str(h, 70)
|
||||
if s and len(s) > 2 and s.isprintable():
|
||||
print(f" {needle.decode():10s} 0x{h:x} {s!r}")
|
||||
shown += 1
|
||||
if shown >= 12:
|
||||
break
|
||||
if not shown:
|
||||
print(f" {needle.decode():10s} (no printable hits)")
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== float constants 0.95 / 0.05 / 0.95f / 0.05f")
|
||||
print("=" * 78)
|
||||
for label, pat in (("double 0.95", struct.pack("<d", 0.95)),
|
||||
("double 0.05", struct.pack("<d", 0.05)),
|
||||
("float 0.95f", struct.pack("<f", 0.95)),
|
||||
("float 0.05f", struct.pack("<f", 0.05))):
|
||||
hits = find_all(pat, blocks=(".rdata", ".data"))
|
||||
print(f" {label:12s} {[hex(h) for h in hits[:10]] or 'ABSENT'}")
|
||||
for h in hits[:6]:
|
||||
xs = xrefs_to(h)
|
||||
for x in xs[:4]:
|
||||
print(f" used at 0x{x[0]:x} in {x[2]} (entry 0x{x[3]:x})")
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("== functions using BOTH 95 (or 5) and 100 as immediates — integer fee math")
|
||||
print("=" * 78)
|
||||
found = []
|
||||
for f in fm.getFunctions(True):
|
||||
ent = int(f.getEntryPoint().getOffset())
|
||||
vals = set()
|
||||
it = listing.getInstructions(f.getBody(), True)
|
||||
while it.hasNext():
|
||||
ins = it.next()
|
||||
if ins.getMnemonicString() not in ("IMUL", "MOV", "CMP", "ADD", "SUB", "LEA", "SHL"):
|
||||
continue
|
||||
for i in range(ins.getNumOperands()):
|
||||
for o in ins.getOpObjects(i):
|
||||
try:
|
||||
v = int(o.getValue())
|
||||
except Exception:
|
||||
continue
|
||||
if v in (95, 100, 5, 20):
|
||||
vals.add(v)
|
||||
if 100 in vals and (95 in vals or 5 in vals):
|
||||
found.append((ent, f.getName(), sorted(vals)))
|
||||
print(f" {len(found)} candidate(s)")
|
||||
for ent, nm, vals in found[:40]:
|
||||
print(f" 0x{ent:x} {nm:26s} immediates {vals}")
|
||||
@@ -1,67 +0,0 @@
|
||||
{
|
||||
"champion": {
|
||||
"bytes": 2,
|
||||
"path": "/ut/game/fifa17/champion",
|
||||
"status": 200
|
||||
},
|
||||
"clubUser": {
|
||||
"bytes": 2,
|
||||
"path": "/ut/game/fifa17/clubUser",
|
||||
"status": 200
|
||||
},
|
||||
"defid": {
|
||||
"bytes": 600,
|
||||
"path": "/ut/game/fifa17/defid?definitionId=200389",
|
||||
"status": 200
|
||||
},
|
||||
"defs_resource": {
|
||||
"bytes": 600,
|
||||
"path": "/ut/game/fifa17/item/resource?resourceId=200389",
|
||||
"status": 200
|
||||
},
|
||||
"draft_state": {
|
||||
"bytes": 118,
|
||||
"path": "/ut/game/fifa17/squad/mode/draft/state",
|
||||
"status": 200
|
||||
},
|
||||
"marketdata": {
|
||||
"bytes": 110,
|
||||
"path": "/ut/game/fifa17/marketdata/pricelimits?defId=200389,200104",
|
||||
"status": 200
|
||||
},
|
||||
"sbs_sets": {
|
||||
"bytes": 537,
|
||||
"path": "/ut/game/fifa17/sbs/sets",
|
||||
"status": 200
|
||||
},
|
||||
"season": {
|
||||
"bytes": 2,
|
||||
"path": "/ut/game/fifa17/season",
|
||||
"status": 200
|
||||
},
|
||||
"squad_0": {
|
||||
"bytes": 7792,
|
||||
"path": "/ut/game/fifa17/squad/0",
|
||||
"status": 200
|
||||
},
|
||||
"tournament": {
|
||||
"bytes": 2,
|
||||
"path": "/ut/game/fifa17/tournament",
|
||||
"status": 200
|
||||
},
|
||||
"user": {
|
||||
"bytes": 751,
|
||||
"path": "/ut/game/fifa17/user",
|
||||
"status": 200
|
||||
},
|
||||
"user_list": {
|
||||
"bytes": 2,
|
||||
"path": "/ut/game/fifa17/user/list",
|
||||
"status": 200
|
||||
},
|
||||
"watchList": {
|
||||
"bytes": 52,
|
||||
"path": "/ut/game/fifa17/watchList",
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"itemData": [
|
||||
{
|
||||
"assetId": 200389,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 70
|
||||
}
|
||||
],
|
||||
"cardType": 0,
|
||||
"cardassetid": 200389,
|
||||
"cardsubtypeid": 0,
|
||||
"commodityId": 200389,
|
||||
"commonName": "Player",
|
||||
"definitionId": 200389,
|
||||
"id": 200389,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"lastName": "Player",
|
||||
"leagueId": 0,
|
||||
"name": "Player",
|
||||
"nation": 0,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CM",
|
||||
"rareflag": 1,
|
||||
"rating": 75,
|
||||
"resourceId": 200389,
|
||||
"teamid": 0,
|
||||
"untradeable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"itemData": [
|
||||
{
|
||||
"assetId": 200389,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 70
|
||||
}
|
||||
],
|
||||
"cardType": 0,
|
||||
"cardassetid": 200389,
|
||||
"cardsubtypeid": 0,
|
||||
"commodityId": 200389,
|
||||
"commonName": "Player",
|
||||
"definitionId": 200389,
|
||||
"id": 200389,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"lastName": "Player",
|
||||
"leagueId": 0,
|
||||
"name": "Player",
|
||||
"nation": 0,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CM",
|
||||
"rareflag": 1,
|
||||
"rating": 75,
|
||||
"resourceId": 200389,
|
||||
"teamid": 0,
|
||||
"untradeable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
[
|
||||
{
|
||||
"gamesWonCurrentMatch": 0,
|
||||
"roundsInfo": [],
|
||||
"squadState": "INVALID",
|
||||
"stateParam1": "INVALID",
|
||||
"stateParam2": "0"
|
||||
}
|
||||
]
|
||||
@@ -1,12 +0,0 @@
|
||||
[
|
||||
{
|
||||
"defId": 200389,
|
||||
"maxPrice": 15000,
|
||||
"minPrice": 150
|
||||
},
|
||||
{
|
||||
"defId": 200104,
|
||||
"maxPrice": 15000,
|
||||
"minPrice": 150
|
||||
}
|
||||
]
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"categories": [
|
||||
{
|
||||
"categoryId": 1,
|
||||
"name": "Foundations",
|
||||
"priority": 1,
|
||||
"sets": [
|
||||
{
|
||||
"awards": [],
|
||||
"categoryId": 1,
|
||||
"challengesCompletedCount": 0,
|
||||
"challengesCount": 1,
|
||||
"description": "Submit an 11-player squad.",
|
||||
"endTime": 4102444800,
|
||||
"hidden": false,
|
||||
"name": "Bronze Challenge",
|
||||
"priority": 1,
|
||||
"setId": 1
|
||||
},
|
||||
{
|
||||
"awards": [],
|
||||
"categoryId": 1,
|
||||
"challengesCompletedCount": 0,
|
||||
"challengesCount": 1,
|
||||
"description": "Get started with your first SBC.",
|
||||
"endTime": 4102444800,
|
||||
"hidden": false,
|
||||
"name": "Simple Start",
|
||||
"priority": 2,
|
||||
"setId": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,707 +0,0 @@
|
||||
{
|
||||
"actives": [],
|
||||
"captain": 100000001,
|
||||
"changed": 0,
|
||||
"chemistry": 49,
|
||||
"custom": "[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,50,50,0,50,40,65,0,65,50,50,1]",
|
||||
"formation": "f433",
|
||||
"id": 0,
|
||||
"kicktakers": [
|
||||
{
|
||||
"dream": false,
|
||||
"id": 100000001,
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"dream": false,
|
||||
"id": 100000001,
|
||||
"index": 1
|
||||
},
|
||||
{
|
||||
"dream": false,
|
||||
"id": 100000001,
|
||||
"index": 2
|
||||
},
|
||||
{
|
||||
"dream": false,
|
||||
"id": 100000001,
|
||||
"index": 3
|
||||
},
|
||||
{
|
||||
"dream": false,
|
||||
"id": 100000001,
|
||||
"index": 4
|
||||
}
|
||||
],
|
||||
"manager": [
|
||||
{
|
||||
"dream": false,
|
||||
"id": 100000427
|
||||
}
|
||||
],
|
||||
"personaId": 33068179,
|
||||
"players": [
|
||||
{
|
||||
"index": 0,
|
||||
"itemData": {
|
||||
"assetId": 200389,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 83
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 77
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 82
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 87
|
||||
}
|
||||
],
|
||||
"cardassetid": 200389,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 200389,
|
||||
"fitness": 99,
|
||||
"id": 100000003,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 44,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "GK",
|
||||
"rareflag": 1,
|
||||
"rating": 87,
|
||||
"resourceId": 200389,
|
||||
"teamid": 240,
|
||||
"untradeable": true
|
||||
},
|
||||
"kitNumber": 1
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"itemData": {
|
||||
"assetId": 197445,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 73
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 81
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 83
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 83
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 73
|
||||
}
|
||||
],
|
||||
"cardassetid": 197445,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 197445,
|
||||
"fitness": 99,
|
||||
"id": 100000006,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 19,
|
||||
"nation": 4,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LB",
|
||||
"rareflag": 1,
|
||||
"rating": 87,
|
||||
"resourceId": 197445,
|
||||
"teamid": 21,
|
||||
"untradeable": true
|
||||
},
|
||||
"kitNumber": 4
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"itemData": {
|
||||
"assetId": 155862,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 78
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 63
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 87
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 83
|
||||
}
|
||||
],
|
||||
"cardassetid": 155862,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 155862,
|
||||
"fitness": 99,
|
||||
"id": 100000005,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 45,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CB",
|
||||
"rareflag": 1,
|
||||
"rating": 89,
|
||||
"resourceId": 155862,
|
||||
"teamid": 243,
|
||||
"untradeable": true
|
||||
},
|
||||
"kitNumber": 3
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"itemData": {
|
||||
"assetId": 182521,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 45
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 80
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 88
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 79
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 69
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 70
|
||||
}
|
||||
],
|
||||
"cardassetid": 182521,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 182521,
|
||||
"fitness": 99,
|
||||
"id": 100000008,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 21,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CM",
|
||||
"rareflag": 1,
|
||||
"rating": 88,
|
||||
"resourceId": 182521,
|
||||
"teamid": 243,
|
||||
"untradeable": true
|
||||
},
|
||||
"kitNumber": 6
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"itemData": {
|
||||
"assetId": 189332,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 93
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 69
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 75
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 83
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 81
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 75
|
||||
}
|
||||
],
|
||||
"cardassetid": 189332,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 189332,
|
||||
"fitness": 99,
|
||||
"id": 100000007,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 45,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LB",
|
||||
"rareflag": 1,
|
||||
"rating": 86,
|
||||
"resourceId": 189332,
|
||||
"teamid": 241,
|
||||
"untradeable": true
|
||||
},
|
||||
"kitNumber": 5
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"itemData": {
|
||||
"assetId": 158023,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 96
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 26
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 61
|
||||
}
|
||||
],
|
||||
"cardassetid": 158023,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 158023,
|
||||
"fitness": 99,
|
||||
"id": 100000002,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 52,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "RW",
|
||||
"rareflag": 1,
|
||||
"rating": 93,
|
||||
"resourceId": 158023,
|
||||
"teamid": 241,
|
||||
"untradeable": true
|
||||
},
|
||||
"kitNumber": 9
|
||||
},
|
||||
{
|
||||
"index": 6,
|
||||
"itemData": {
|
||||
"assetId": 183907,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 79
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 72
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 68
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 85
|
||||
}
|
||||
],
|
||||
"cardassetid": 183907,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 183907,
|
||||
"fitness": 99,
|
||||
"id": 100000004,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 19,
|
||||
"nation": 21,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CB",
|
||||
"rareflag": 1,
|
||||
"rating": 90,
|
||||
"resourceId": 183907,
|
||||
"teamid": 21,
|
||||
"untradeable": true
|
||||
},
|
||||
"kitNumber": 2
|
||||
},
|
||||
{
|
||||
"index": 7,
|
||||
"itemData": {
|
||||
"assetId": 183277,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 81
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 82
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 91
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 32
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 64
|
||||
}
|
||||
],
|
||||
"cardassetid": 183277,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 183277,
|
||||
"fitness": 99,
|
||||
"id": 100000009,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 13,
|
||||
"nation": 7,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LM",
|
||||
"rareflag": 1,
|
||||
"rating": 88,
|
||||
"resourceId": 183277,
|
||||
"teamid": 5,
|
||||
"untradeable": true
|
||||
},
|
||||
"kitNumber": 7
|
||||
},
|
||||
{
|
||||
"index": 8,
|
||||
"itemData": {
|
||||
"assetId": 176580,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 82
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 79
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 87
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 42
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 79
|
||||
}
|
||||
],
|
||||
"cardassetid": 176580,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 176580,
|
||||
"fitness": 99,
|
||||
"id": 100000010,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 60,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "ST",
|
||||
"rareflag": 1,
|
||||
"rating": 92,
|
||||
"resourceId": 176580,
|
||||
"teamid": 241,
|
||||
"untradeable": true
|
||||
},
|
||||
"kitNumber": 10
|
||||
},
|
||||
{
|
||||
"index": 9,
|
||||
"itemData": {
|
||||
"assetId": 188545,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 81
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 87
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 74
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 85
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 38
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 82
|
||||
}
|
||||
],
|
||||
"cardassetid": 188545,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 188545,
|
||||
"fitness": 99,
|
||||
"id": 100000025,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 19,
|
||||
"nation": 37,
|
||||
"owners": 1,
|
||||
"pile": "club",
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "ST",
|
||||
"rareflag": 1,
|
||||
"rating": 90,
|
||||
"resourceId": 188545,
|
||||
"teamid": 21,
|
||||
"untradeable": true
|
||||
},
|
||||
"kitNumber": 11
|
||||
},
|
||||
{
|
||||
"index": 10,
|
||||
"itemData": {
|
||||
"assetId": 20801,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 81
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 91
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 33
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 80
|
||||
}
|
||||
],
|
||||
"cardassetid": 20801,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 20801,
|
||||
"fitness": 99,
|
||||
"id": 100000001,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 38,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LW",
|
||||
"rareflag": 1,
|
||||
"rating": 94,
|
||||
"resourceId": 20801,
|
||||
"teamid": 243,
|
||||
"untradeable": true
|
||||
},
|
||||
"kitNumber": 8
|
||||
},
|
||||
{
|
||||
"index": 11,
|
||||
"itemData": {
|
||||
"dream": false,
|
||||
"id": 0
|
||||
},
|
||||
"kitNumber": 0
|
||||
},
|
||||
{
|
||||
"index": 12,
|
||||
"itemData": {
|
||||
"dream": false,
|
||||
"id": 0
|
||||
},
|
||||
"kitNumber": 0
|
||||
},
|
||||
{
|
||||
"index": 13,
|
||||
"itemData": {
|
||||
"dream": false,
|
||||
"id": 0
|
||||
},
|
||||
"kitNumber": 0
|
||||
},
|
||||
{
|
||||
"index": 14,
|
||||
"itemData": {
|
||||
"dream": false,
|
||||
"id": 0
|
||||
},
|
||||
"kitNumber": 0
|
||||
},
|
||||
{
|
||||
"index": 15,
|
||||
"itemData": {
|
||||
"dream": false,
|
||||
"id": 0
|
||||
},
|
||||
"kitNumber": 0
|
||||
},
|
||||
{
|
||||
"index": 16,
|
||||
"itemData": {
|
||||
"dream": false,
|
||||
"id": 0
|
||||
},
|
||||
"kitNumber": 0
|
||||
},
|
||||
{
|
||||
"index": 17,
|
||||
"itemData": {
|
||||
"dream": false,
|
||||
"id": 0
|
||||
},
|
||||
"kitNumber": 0
|
||||
},
|
||||
{
|
||||
"index": 18,
|
||||
"itemData": {
|
||||
"dream": false,
|
||||
"id": 0
|
||||
},
|
||||
"kitNumber": 0
|
||||
},
|
||||
{
|
||||
"index": 19,
|
||||
"itemData": {
|
||||
"dream": false,
|
||||
"id": 0
|
||||
},
|
||||
"kitNumber": 0
|
||||
},
|
||||
{
|
||||
"index": 20,
|
||||
"itemData": {
|
||||
"dream": false,
|
||||
"id": 0
|
||||
},
|
||||
"kitNumber": 0
|
||||
},
|
||||
{
|
||||
"index": 21,
|
||||
"itemData": {
|
||||
"dream": false,
|
||||
"id": 0
|
||||
},
|
||||
"kitNumber": 0
|
||||
},
|
||||
{
|
||||
"index": 22,
|
||||
"itemData": {
|
||||
"dream": false,
|
||||
"id": 0
|
||||
},
|
||||
"kitNumber": 0
|
||||
}
|
||||
],
|
||||
"rating": 90,
|
||||
"squadName": "OpenFUT",
|
||||
"squadType": "REGULAR_SQUAD",
|
||||
"starRating": 90
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,54 +0,0 @@
|
||||
{
|
||||
"userInfo": {
|
||||
"accountCreatedPlatformName": "pc",
|
||||
"actives": [],
|
||||
"bidTokens": {
|
||||
"count": 0,
|
||||
"updateTime": 0
|
||||
},
|
||||
"clubAbbr": "OFC",
|
||||
"clubName": "OpenFUT",
|
||||
"clubNameChangeAllowed": false,
|
||||
"currencies": [
|
||||
{
|
||||
"active": true,
|
||||
"finalFunds": 29876776,
|
||||
"funds": 29876776,
|
||||
"name": "coins"
|
||||
},
|
||||
{
|
||||
"active": true,
|
||||
"finalFunds": 0,
|
||||
"funds": 0,
|
||||
"name": "points"
|
||||
}
|
||||
],
|
||||
"divisionOffline": 10,
|
||||
"divisionOnline": 10,
|
||||
"draw": 0,
|
||||
"established": "2026",
|
||||
"feature": {},
|
||||
"loss": 0,
|
||||
"personaId": 33068179,
|
||||
"purchased": false,
|
||||
"reliability": {
|
||||
"matchUnfinishedTime": 0,
|
||||
"reliability": 100
|
||||
},
|
||||
"sessionCoinsBankBalance": 0,
|
||||
"squadList": {
|
||||
"squad": [
|
||||
{
|
||||
"chemistry": 49,
|
||||
"formation": "f433",
|
||||
"id": 0,
|
||||
"rating": 89,
|
||||
"squadName": "OpenFUT",
|
||||
"squadType": "REGULAR_SQUAD"
|
||||
}
|
||||
]
|
||||
},
|
||||
"trophies": 0,
|
||||
"won": 0
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"auctionInfo": [],
|
||||
"credits": 29876776,
|
||||
"total": 0
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "0",
|
||||
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "highest"
|
||||
},
|
||||
"surfaces": {
|
||||
"counts": {
|
||||
"body": {
|
||||
"count": 0,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 0,
|
||||
"sold": 1
|
||||
},
|
||||
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||
"status": 200
|
||||
},
|
||||
"tradePile": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143,
|
||||
"duplicateItemIdList": [],
|
||||
"total": 1
|
||||
},
|
||||
"sha256": "caecc67a615ddd0f209b36c7c8ea6a04e0002552d9a42b8eeca20df84feedf3c",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_filtered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_unfiltered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "0",
|
||||
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "buyNow"
|
||||
},
|
||||
"surfaces": {
|
||||
"counts": {
|
||||
"body": {
|
||||
"count": 0,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 0,
|
||||
"sold": 1
|
||||
},
|
||||
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||
"status": 200
|
||||
},
|
||||
"tradePile": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "buyNow",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143,
|
||||
"duplicateItemIdList": [],
|
||||
"total": 1
|
||||
},
|
||||
"sha256": "0b6e5cc5ab58fdbdd76586b1cff685a562e669fd37750cbd1397db9ee9e97407",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_filtered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "buyNow",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "fab8d43b7ac0f0e09d6902155f7c5f6d91e57b4b595ec8f6093a5a9dcb02bcdb",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_unfiltered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "buyNow",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "fab8d43b7ac0f0e09d6902155f7c5f6d91e57b4b595ec8f6093a5a9dcb02bcdb",
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "0",
|
||||
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "highest"
|
||||
},
|
||||
"surfaces": {
|
||||
"counts": {
|
||||
"body": {
|
||||
"count": 0,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 0,
|
||||
"sold": 1
|
||||
},
|
||||
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||
"status": 200
|
||||
},
|
||||
"tradePile": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143,
|
||||
"duplicateItemIdList": [],
|
||||
"total": 1
|
||||
},
|
||||
"sha256": "caecc67a615ddd0f209b36c7c8ea6a04e0002552d9a42b8eeca20df84feedf3c",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_filtered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_unfiltered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "1",
|
||||
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "highest"
|
||||
},
|
||||
"surfaces": {
|
||||
"counts": {
|
||||
"body": {
|
||||
"count": 0,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 0,
|
||||
"sold": 1
|
||||
},
|
||||
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||
"status": 200
|
||||
},
|
||||
"tradePile": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 1,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143,
|
||||
"duplicateItemIdList": [],
|
||||
"total": 1
|
||||
},
|
||||
"sha256": "6e228a34b5d7fac6bf70710cbd4ca35b58740e04ae455ffeee04b6ce7faab8e5",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_filtered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 1,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "c1557013bf20285d0512ca0d055a16df8036260559d640da11102a2379ac5986",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_unfiltered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 1,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "c1557013bf20285d0512ca0d055a16df8036260559d640da11102a2379ac5986",
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,509 +0,0 @@
|
||||
{
|
||||
"counts_states": {
|
||||
"S1_zero_active_one_sold": {
|
||||
"count": 0,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 0,
|
||||
"sold": 1
|
||||
},
|
||||
"S2_one_active_one_sold_count_active": {
|
||||
"count": 1,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 1,
|
||||
"sold": 1
|
||||
},
|
||||
"S2_one_active_one_sold_count_membership": {
|
||||
"count": 2,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 1,
|
||||
"sold": 1
|
||||
}
|
||||
},
|
||||
"diffs": {
|
||||
"orthogonal_coinsprocessed": [
|
||||
{
|
||||
"a": 0,
|
||||
"b": 1,
|
||||
"kind": "VALUE_MISMATCH",
|
||||
"path": "auctionInfo[0].coinsProcessed"
|
||||
}
|
||||
],
|
||||
"primary_bidstate": [
|
||||
{
|
||||
"a": "highest",
|
||||
"b": "buyNow",
|
||||
"kind": "VALUE_MISMATCH",
|
||||
"path": "auctionInfo[0].bidState"
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated": "2026-08-18T02:19:25Z",
|
||||
"settlement": {
|
||||
"buyer": 19850,
|
||||
"fee": 7,
|
||||
"instances": 1,
|
||||
"owner": "club-buyer-b",
|
||||
"seller": 1143
|
||||
},
|
||||
"variants": {
|
||||
"A_highest": {
|
||||
"env": {
|
||||
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "0",
|
||||
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "highest"
|
||||
},
|
||||
"surfaces": {
|
||||
"counts": {
|
||||
"body": {
|
||||
"count": 0,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 0,
|
||||
"sold": 1
|
||||
},
|
||||
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||
"status": 200
|
||||
},
|
||||
"tradePile": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143,
|
||||
"duplicateItemIdList": [],
|
||||
"total": 1
|
||||
},
|
||||
"sha256": "caecc67a615ddd0f209b36c7c8ea6a04e0002552d9a42b8eeca20df84feedf3c",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_filtered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_unfiltered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
},
|
||||
"B_buyNow": {
|
||||
"env": {
|
||||
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "0",
|
||||
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "buyNow"
|
||||
},
|
||||
"surfaces": {
|
||||
"counts": {
|
||||
"body": {
|
||||
"count": 0,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 0,
|
||||
"sold": 1
|
||||
},
|
||||
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||
"status": 200
|
||||
},
|
||||
"tradePile": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "buyNow",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143,
|
||||
"duplicateItemIdList": [],
|
||||
"total": 1
|
||||
},
|
||||
"sha256": "0b6e5cc5ab58fdbdd76586b1cff685a562e669fd37750cbd1397db9ee9e97407",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_filtered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "buyNow",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "fab8d43b7ac0f0e09d6902155f7c5f6d91e57b4b595ec8f6093a5a9dcb02bcdb",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_unfiltered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "buyNow",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "fab8d43b7ac0f0e09d6902155f7c5f6d91e57b4b595ec8f6093a5a9dcb02bcdb",
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
},
|
||||
"C_cp0": {
|
||||
"env": {
|
||||
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "0",
|
||||
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "highest"
|
||||
},
|
||||
"surfaces": {
|
||||
"counts": {
|
||||
"body": {
|
||||
"count": 0,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 0,
|
||||
"sold": 1
|
||||
},
|
||||
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||
"status": 200
|
||||
},
|
||||
"tradePile": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143,
|
||||
"duplicateItemIdList": [],
|
||||
"total": 1
|
||||
},
|
||||
"sha256": "caecc67a615ddd0f209b36c7c8ea6a04e0002552d9a42b8eeca20df84feedf3c",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_filtered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_unfiltered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
},
|
||||
"D_cp1": {
|
||||
"env": {
|
||||
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "1",
|
||||
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "highest"
|
||||
},
|
||||
"surfaces": {
|
||||
"counts": {
|
||||
"body": {
|
||||
"count": 0,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 0,
|
||||
"sold": 1
|
||||
},
|
||||
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||
"status": 200
|
||||
},
|
||||
"tradePile": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 1,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143,
|
||||
"duplicateItemIdList": [],
|
||||
"total": 1
|
||||
},
|
||||
"sha256": "6e228a34b5d7fac6bf70710cbd4ca35b58740e04ae455ffeee04b6ce7faab8e5",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_filtered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 1,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "c1557013bf20285d0512ca0d055a16df8036260559d640da11102a2379ac5986",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_unfiltered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 1,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 212188,
|
||||
"id": 100000178,
|
||||
"itemState": "forSale",
|
||||
"preferredPosition": "ST",
|
||||
"rating": 75,
|
||||
"resourceId": 212188,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "c1557013bf20285d0512ca0d055a16df8036260559d640da11102a2379ac5986",
|
||||
"status": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,850 +0,0 @@
|
||||
{
|
||||
"captured": "2026-08-18T02:21:49Z",
|
||||
"phase": "0 \u2014 staging validated, sold row present, awaiting live client",
|
||||
"settlement": {
|
||||
"clubs": {
|
||||
"club-buyer-b": 19850,
|
||||
"club-seller-a-cage": 1143
|
||||
},
|
||||
"disposable_owner": [
|
||||
"club-buyer-b"
|
||||
],
|
||||
"fee": 7,
|
||||
"gross": 150,
|
||||
"instances": 1,
|
||||
"proceeds": 143
|
||||
},
|
||||
"sold_row": {
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 232273,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 62
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 28
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 33
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 49
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 53
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 50
|
||||
}
|
||||
],
|
||||
"cardassetid": 232273,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 232273,
|
||||
"discardValue": 150,
|
||||
"fitness": 99,
|
||||
"id": 100000012,
|
||||
"itemState": "forSale",
|
||||
"itemType": "player",
|
||||
"leagueId": 32,
|
||||
"nation": 117,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LB",
|
||||
"rareflag": 1,
|
||||
"rating": 51,
|
||||
"resourceId": 232273,
|
||||
"teamid": 112487,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
},
|
||||
"surfaces": {
|
||||
"club": {
|
||||
"body": {
|
||||
"itemData": [
|
||||
{
|
||||
"assetId": 20801,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 97
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 97
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 96
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 38
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 85
|
||||
}
|
||||
],
|
||||
"cardassetid": 20801,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 83906881,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000001,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 38,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LW",
|
||||
"rareflag": 11,
|
||||
"rating": 99,
|
||||
"resourceId": 83906881,
|
||||
"teamid": 243,
|
||||
"untradeable": false
|
||||
},
|
||||
{
|
||||
"assetId": 158023,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 94
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 95
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 91
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 99
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 31
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 66
|
||||
}
|
||||
],
|
||||
"cardassetid": 158023,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 84044103,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000002,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 52,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "RW",
|
||||
"rareflag": 11,
|
||||
"rating": 98,
|
||||
"resourceId": 84044103,
|
||||
"teamid": 241,
|
||||
"untradeable": false
|
||||
},
|
||||
{
|
||||
"assetId": 167495,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 94
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 95
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 99
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 94
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 64
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 96
|
||||
}
|
||||
],
|
||||
"cardassetid": 167495,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 84053575,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000003,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 19,
|
||||
"nation": 21,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "GK",
|
||||
"rareflag": 11,
|
||||
"rating": 97,
|
||||
"resourceId": 84053575,
|
||||
"teamid": 21,
|
||||
"untradeable": false
|
||||
},
|
||||
{
|
||||
"assetId": 176580,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 85
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 93
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 82
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 45
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 82
|
||||
}
|
||||
],
|
||||
"cardassetid": 176580,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 117617092,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000004,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 60,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "ST",
|
||||
"rareflag": 22,
|
||||
"rating": 95,
|
||||
"resourceId": 117617092,
|
||||
"teamid": 241,
|
||||
"untradeable": false
|
||||
},
|
||||
{
|
||||
"assetId": 177003,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 78
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 79
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 93
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 76
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 70
|
||||
}
|
||||
],
|
||||
"cardassetid": 177003,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 151171947,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000005,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 10,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CM",
|
||||
"rareflag": 24,
|
||||
"rating": 93,
|
||||
"resourceId": 151171947,
|
||||
"teamid": 243,
|
||||
"untradeable": false
|
||||
},
|
||||
{
|
||||
"assetId": 164240,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 77
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 60
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 76
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 76
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 93
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 82
|
||||
}
|
||||
],
|
||||
"cardassetid": 164240,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 134381968,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000006,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 16,
|
||||
"nation": 54,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CB",
|
||||
"rareflag": 23,
|
||||
"rating": 92,
|
||||
"resourceId": 134381968,
|
||||
"teamid": 73,
|
||||
"untradeable": false
|
||||
},
|
||||
{
|
||||
"assetId": 182493,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 69
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 52
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 69
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 67
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 92
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 83
|
||||
}
|
||||
],
|
||||
"cardassetid": 182493,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 151177437,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000007,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 60,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CB",
|
||||
"rareflag": 24,
|
||||
"rating": 92,
|
||||
"resourceId": 151177437,
|
||||
"teamid": 240,
|
||||
"untradeable": false
|
||||
},
|
||||
{
|
||||
"assetId": 168651,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 71
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 89
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 91
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 87
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 64
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 71
|
||||
}
|
||||
],
|
||||
"cardassetid": 168651,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 84054731,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000008,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 10,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CM",
|
||||
"rareflag": 11,
|
||||
"rating": 92,
|
||||
"resourceId": 84054731,
|
||||
"teamid": 241,
|
||||
"untradeable": false
|
||||
},
|
||||
{
|
||||
"assetId": 182521,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 48
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 83
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 91
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 82
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 72
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 73
|
||||
}
|
||||
],
|
||||
"cardassetid": 182521,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 134400249,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000009,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 53,
|
||||
"nation": 21,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "CM",
|
||||
"rareflag": 23,
|
||||
"rating": 91,
|
||||
"resourceId": 134400249,
|
||||
"teamid": 243,
|
||||
"untradeable": false
|
||||
},
|
||||
{
|
||||
"assetId": 197445,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 90
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 77
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 85
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 87
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 87
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 77
|
||||
}
|
||||
],
|
||||
"cardassetid": 197445,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 151192389,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000010,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 19,
|
||||
"nation": 4,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LB",
|
||||
"rareflag": 24,
|
||||
"rating": 91,
|
||||
"resourceId": 151192389,
|
||||
"teamid": 21,
|
||||
"untradeable": false
|
||||
},
|
||||
{
|
||||
"assetId": 121939,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 70
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 58
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 84
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 86
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 88
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 66
|
||||
}
|
||||
],
|
||||
"cardassetid": 121939,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 100785235,
|
||||
"discardValue": 1500,
|
||||
"fitness": 99,
|
||||
"id": 100000011,
|
||||
"itemState": "free",
|
||||
"itemType": "player",
|
||||
"leagueId": 19,
|
||||
"nation": 21,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "RB",
|
||||
"rareflag": 21,
|
||||
"rating": 90,
|
||||
"resourceId": 100785235,
|
||||
"teamid": 21,
|
||||
"untradeable": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"sha256": "0a916e8e129a9c05294784738c305d651beedd58ebd914a6abfa94bd0a59abc7",
|
||||
"status": 200
|
||||
},
|
||||
"counts": {
|
||||
"body": {
|
||||
"count": 0,
|
||||
"maxAuctionsAllowed": 100,
|
||||
"offered": 0,
|
||||
"selling": 0,
|
||||
"sold": 1
|
||||
},
|
||||
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||
"status": 200
|
||||
},
|
||||
"tradePile": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 232273,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 62
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 28
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 33
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 49
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 53
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 50
|
||||
}
|
||||
],
|
||||
"cardassetid": 232273,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 232273,
|
||||
"discardValue": 150,
|
||||
"fitness": 99,
|
||||
"id": 100000012,
|
||||
"itemState": "forSale",
|
||||
"itemType": "player",
|
||||
"leagueId": 32,
|
||||
"nation": 117,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LB",
|
||||
"rareflag": 1,
|
||||
"rating": 51,
|
||||
"resourceId": 232273,
|
||||
"teamid": 112487,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143,
|
||||
"duplicateItemIdList": [],
|
||||
"total": 1
|
||||
},
|
||||
"sha256": "c792ebcf179a5c617ffe02859ad39eba830a4d5b5224a0953ecef774ee4cf75b",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_filtered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 232273,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 62
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 28
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 33
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 49
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 53
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 50
|
||||
}
|
||||
],
|
||||
"cardassetid": 232273,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 232273,
|
||||
"discardValue": 150,
|
||||
"fitness": 99,
|
||||
"id": 100000012,
|
||||
"itemState": "forSale",
|
||||
"itemType": "player",
|
||||
"leagueId": 32,
|
||||
"nation": 117,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LB",
|
||||
"rareflag": 1,
|
||||
"rating": 51,
|
||||
"resourceId": 232273,
|
||||
"teamid": 112487,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "db79d9fcfc47ae680b44311598e82918b8afd3284160143c33e525adf693f30e",
|
||||
"status": 200
|
||||
},
|
||||
"trade_status_unfiltered": {
|
||||
"body": {
|
||||
"auctionInfo": [
|
||||
{
|
||||
"bidState": "highest",
|
||||
"buyNowPrice": 150,
|
||||
"coinsProcessed": 0,
|
||||
"currentBid": 150,
|
||||
"expires": 0,
|
||||
"itemData": {
|
||||
"assetId": 232273,
|
||||
"attributeList": [
|
||||
{
|
||||
"index": 0,
|
||||
"value": 62
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"value": 28
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"value": 33
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"value": 49
|
||||
},
|
||||
{
|
||||
"index": 4,
|
||||
"value": 53
|
||||
},
|
||||
{
|
||||
"index": 5,
|
||||
"value": 50
|
||||
}
|
||||
],
|
||||
"cardassetid": 232273,
|
||||
"cardsubtypeid": 0,
|
||||
"contract": 7,
|
||||
"definitionId": 232273,
|
||||
"discardValue": 150,
|
||||
"fitness": 99,
|
||||
"id": 100000012,
|
||||
"itemState": "forSale",
|
||||
"itemType": "player",
|
||||
"leagueId": 32,
|
||||
"nation": 117,
|
||||
"owners": 1,
|
||||
"playStyle": 250,
|
||||
"preferredPosition": "LB",
|
||||
"rareflag": 1,
|
||||
"rating": 51,
|
||||
"resourceId": 232273,
|
||||
"teamid": 112487,
|
||||
"untradeable": false
|
||||
},
|
||||
"sellerEstablished": 1,
|
||||
"sellerName": "CAGE",
|
||||
"startingBid": 150,
|
||||
"tradeId": 900500150,
|
||||
"tradeState": "closed",
|
||||
"watched": false
|
||||
}
|
||||
],
|
||||
"credits": 1143
|
||||
},
|
||||
"sha256": "db79d9fcfc47ae680b44311598e82918b8afd3284160143c33e525adf693f30e",
|
||||
"status": 200
|
||||
}
|
||||
},
|
||||
"topology": {
|
||||
"advertises_utas": "http://<host>:8299/",
|
||||
"blaze": [
|
||||
42327,
|
||||
42330,
|
||||
42331
|
||||
],
|
||||
"core": 18081,
|
||||
"utas_host": 8299
|
||||
},
|
||||
"variant": "A_highest (bidState=highest, coinsProcessed=0, countMode=active)"
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
{
|
||||
"purchase": [
|
||||
{
|
||||
"assetId": 1,
|
||||
"id": 1,
|
||||
"packType": "BRONZE",
|
||||
"description": "Bronze Pack",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 1,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 400,
|
||||
"finalFunds": 400
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 4,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 4,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 5,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 0,
|
||||
"rareQuantity": 0,
|
||||
"itemQuantity": 5
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "bronze"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 5,
|
||||
"id": 5,
|
||||
"packType": "GOLD",
|
||||
"description": "Gold Pack",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 2,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 5000,
|
||||
"finalFunds": 5000
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 50,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 50,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 7,
|
||||
"rareQuantity": 7,
|
||||
"itemQuantity": 7
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "gold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 6,
|
||||
"id": 6,
|
||||
"packType": "GOLD",
|
||||
"description": "Premium Gold",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 3,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 15000,
|
||||
"finalFunds": 15000
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 150,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 150,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 11,
|
||||
"rareQuantity": 11,
|
||||
"itemQuantity": 11
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "gold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 7,
|
||||
"id": 7,
|
||||
"packType": "GOLD",
|
||||
"description": "Special Players Pack",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 4,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 25000,
|
||||
"finalFunds": 25000
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 250,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 250,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 11,
|
||||
"rareQuantity": 11,
|
||||
"itemQuantity": 11
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "special"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 65534,
|
||||
"id": 65534,
|
||||
"packType": "GOLD",
|
||||
"description": "",
|
||||
"state": "inactive",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 1,
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 0,
|
||||
"rareQuantity": 0,
|
||||
"itemQuantity": 0
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "mypacks",
|
||||
"priority": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"timestamp": 1596326400
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
{
|
||||
"purchase": [
|
||||
{
|
||||
"assetId": 1,
|
||||
"id": 1,
|
||||
"packType": "BRONZE",
|
||||
"description": "Bronze Pack",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 1,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 400,
|
||||
"finalFunds": 400
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 4,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 4,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 5,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 0,
|
||||
"rareQuantity": 0,
|
||||
"itemQuantity": 5
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "bronze"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 5,
|
||||
"id": 5,
|
||||
"packType": "GOLD",
|
||||
"description": "Gold Pack",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 2,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 5000,
|
||||
"finalFunds": 5000
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 50,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 50,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 7,
|
||||
"rareQuantity": 7,
|
||||
"itemQuantity": 7
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "gold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 6,
|
||||
"id": 6,
|
||||
"packType": "GOLD",
|
||||
"description": "Premium Gold",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 3,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 15000,
|
||||
"finalFunds": 15000
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 150,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 150,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 11,
|
||||
"rareQuantity": 11,
|
||||
"itemQuantity": 11
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "gold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 7,
|
||||
"id": 7,
|
||||
"packType": "GOLD",
|
||||
"description": "Special Players Pack",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 4,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 25000,
|
||||
"finalFunds": 25000
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 250,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 250,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 11,
|
||||
"rareQuantity": 11,
|
||||
"itemQuantity": 11
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "special"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 65534,
|
||||
"id": 65534,
|
||||
"packType": "GOLD",
|
||||
"description": "",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 1,
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 0,
|
||||
"rareQuantity": 0,
|
||||
"itemQuantity": 0
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "mypacks",
|
||||
"priority": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"timestamp": 1596326400
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"purchase": [{"assetId": 1, "id": 1, "packType": "BRONZE", "description": "Bronze Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 1, "currencies": [{"name": "coins", "funds": 400, "finalFunds": 400}], "extPrice": {"finalPrice": {"amount": 4, "currency": "mtx"}, "originalPrice": {"amount": 4, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 5, "silverQuantity": 0, "goldQuantity": 0, "rareQuantity": 0, "itemQuantity": 5}, "unopened": false, "displayGroup": {"value": "bronze"}}, {"assetId": 5, "id": 5, "packType": "GOLD", "description": "Gold Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 2, "currencies": [{"name": "coins", "funds": 5000, "finalFunds": 5000}], "extPrice": {"finalPrice": {"amount": 50, "currency": "mtx"}, "originalPrice": {"amount": 50, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 7, "rareQuantity": 7, "itemQuantity": 7}, "unopened": false, "displayGroup": {"value": "gold"}}, {"assetId": 6, "id": 6, "packType": "GOLD", "description": "Premium Gold", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 3, "currencies": [{"name": "coins", "funds": 15000, "finalFunds": 15000}], "extPrice": {"finalPrice": {"amount": 150, "currency": "mtx"}, "originalPrice": {"amount": 150, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 11, "rareQuantity": 11, "itemQuantity": 11}, "unopened": false, "displayGroup": {"value": "gold"}}, {"assetId": 7, "id": 7, "packType": "GOLD", "description": "Special Players Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 4, "currencies": [{"name": "coins", "funds": 25000, "finalFunds": 25000}], "extPrice": {"finalPrice": {"amount": 250, "currency": "mtx"}, "originalPrice": {"amount": 250, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 11, "rareQuantity": 11, "itemQuantity": 11}, "unopened": false, "displayGroup": {"value": "special"}}], "timestamp": 1596326400}
|
||||
@@ -1,173 +0,0 @@
|
||||
{
|
||||
"purchase": [
|
||||
{
|
||||
"assetId": 1,
|
||||
"id": 1,
|
||||
"packType": "BRONZE",
|
||||
"description": "Bronze Pack",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 1,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 400,
|
||||
"finalFunds": 400
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 4,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 4,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 5,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 0,
|
||||
"rareQuantity": 0,
|
||||
"itemQuantity": 5
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "bronze"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 5,
|
||||
"id": 5,
|
||||
"packType": "GOLD",
|
||||
"description": "Gold Pack",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 2,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 5000,
|
||||
"finalFunds": 5000
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 50,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 50,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 7,
|
||||
"rareQuantity": 7,
|
||||
"itemQuantity": 7
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "gold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 6,
|
||||
"id": 6,
|
||||
"packType": "GOLD",
|
||||
"description": "Premium Gold",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 3,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 15000,
|
||||
"finalFunds": 15000
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 150,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 150,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 11,
|
||||
"rareQuantity": 11,
|
||||
"itemQuantity": 11
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "gold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 7,
|
||||
"id": 7,
|
||||
"packType": "GOLD",
|
||||
"description": "Special Players Pack",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 4,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 25000,
|
||||
"finalFunds": 25000
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 250,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 250,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 11,
|
||||
"rareQuantity": 11,
|
||||
"itemQuantity": 11
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "special"
|
||||
}
|
||||
}
|
||||
],
|
||||
"timestamp": 1596326400
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
{"purchase": [{"assetId": 1, "id": 1, "packType": "BRONZE", "description": "Bronze Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 1, "currencies": [{"name": "coins", "funds": 400, "finalFunds": 400}], "extPrice": {"finalPrice": {"amount": 4, "currency": "mtx"}, "originalPrice": {"amount": 4, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 5, "silverQuantity": 0, "goldQuantity": 0, "rareQuantity": 0, "itemQuantity": 5}, "unopened": false, "displayGroup": {"value": "bronze"}}, {"assetId": 5, "id": 5, "packType": "GOLD", "description": "Gold Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 2, "currencies": [{"name": "coins", "funds": 5000, "finalFunds": 5000}], "extPrice": {"finalPrice": {"amount": 50, "currency": "mtx"}, "originalPrice": {"amount": 50, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 7, "rareQuantity": 7, "itemQuantity": 7}, "unopened": false, "displayGroup": {"value": "gold"}}, {"assetId": 6, "id": 6, "packType": "GOLD", "description": "Premium Gold", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 3, "currencies": [{"name": "coins", "funds": 15000, "finalFunds": 15000}], "extPrice": {"finalPrice": {"amount": 150, "currency": "mtx"}, "originalPrice": {"amount": 150, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 11, "rareQuantity": 11, "itemQuantity": 11}, "unopened": false, "displayGroup": {"value": "gold"}}, {"assetId": 7, "id": 7, "packType": "GOLD", "description": "Special Players Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 4, "currencies": [{"name": "coins", "funds": 25000, "finalFunds": 25000}], "extPrice": {"finalPrice": {"amount": 250, "currency": "mtx"}, "originalPrice": {"amount": 250, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 11, "rareQuantity": 11, "itemQuantity": 11}, "unopened": false, "displayGroup": {"value": "special"}}], "timestamp": 1596326400}
|
||||
@@ -1,199 +0,0 @@
|
||||
{
|
||||
"purchase": [
|
||||
{
|
||||
"assetId": 1,
|
||||
"id": 1,
|
||||
"packType": "BRONZE",
|
||||
"description": "Bronze Pack",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 1,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 400,
|
||||
"finalFunds": 400
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 4,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 4,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 5,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 0,
|
||||
"rareQuantity": 0,
|
||||
"itemQuantity": 5
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "bronze"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 5,
|
||||
"id": 5,
|
||||
"packType": "GOLD",
|
||||
"description": "Gold Pack",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 2,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 5000,
|
||||
"finalFunds": 5000
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 50,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 50,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 7,
|
||||
"rareQuantity": 7,
|
||||
"itemQuantity": 7
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "gold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 6,
|
||||
"id": 6,
|
||||
"packType": "GOLD",
|
||||
"description": "Premium Gold",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 3,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 15000,
|
||||
"finalFunds": 15000
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 150,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 150,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 11,
|
||||
"rareQuantity": 11,
|
||||
"itemQuantity": 11
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "gold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 7,
|
||||
"id": 7,
|
||||
"packType": "GOLD",
|
||||
"description": "Special Players Pack",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 4,
|
||||
"currencies": [
|
||||
{
|
||||
"name": "coins",
|
||||
"funds": 25000,
|
||||
"finalFunds": 25000
|
||||
}
|
||||
],
|
||||
"extPrice": {
|
||||
"finalPrice": {
|
||||
"amount": 250,
|
||||
"currency": "mtx"
|
||||
},
|
||||
"originalPrice": {
|
||||
"amount": 250,
|
||||
"currency": "mtx"
|
||||
}
|
||||
},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 11,
|
||||
"rareQuantity": 11,
|
||||
"itemQuantity": 11
|
||||
},
|
||||
"unopened": false,
|
||||
"displayGroup": {
|
||||
"value": "special"
|
||||
}
|
||||
},
|
||||
{
|
||||
"assetId": 70,
|
||||
"id": 70,
|
||||
"packType": "GOLD",
|
||||
"description": "Reward Special Players Pack",
|
||||
"state": "active",
|
||||
"saleType": "promo",
|
||||
"limitType": "NONE",
|
||||
"quantity": 0,
|
||||
"purchaseLimit": 0,
|
||||
"purchaseCount": 0,
|
||||
"isPremium": false,
|
||||
"sortPriority": 1,
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0,
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": 11,
|
||||
"rareQuantity": 11,
|
||||
"itemQuantity": 11
|
||||
},
|
||||
"unopened": true,
|
||||
"displayGroup": {
|
||||
"value": "mypacks",
|
||||
"priority": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"timestamp": 1596326400
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
# FIFA 23 PC Startup Flow (Offline / Proton)
|
||||
|
||||
Observed via FLE log, hook log, and file inspection on 2026-06-26.
|
||||
|
||||
## Launch chain
|
||||
|
||||
```
|
||||
umu-run / Steam → FIFA23.exe (via Proton/Wine)
|
||||
│
|
||||
├─ DLL load order (before entry point)
|
||||
│ ntdll.dll, kernel32.dll, ws2_32.dll …
|
||||
│ version.dll ← our hook DLL slot (loads here)
|
||||
│ FIFALiveEditor.DLL ← injected by FLE launcher after ~100 ms
|
||||
│
|
||||
├─ anadius / LSX emulator (anadius64.dll)
|
||||
│ Fakes EA App / Origin session
|
||||
│ Reads HKLM\SOFTWARE\Wow6432Node\Origin\ClientPath
|
||||
│ Writes AppData\Local\anadius\LSX emu\achievement-*.xml
|
||||
│ Provides fake PersonaId=1144668899 / UserId=1000200030000
|
||||
│
|
||||
├─ EA Anti-Cheat (EAAntiCheat.GameServiceLauncher.exe)
|
||||
│ Spawns as child; checks EAAntiCheat.cfg
|
||||
│ Not active in offline/cracked builds (FakeEAACLauncher present)
|
||||
│
|
||||
└─ FIFA23.exe entry point
|
||||
Frostbite engine init (BuildDate 2023-07-05, changelist 5417699)
|
||||
Reads Data\initfs_Win32 ← Frostbite package manifest
|
||||
Reads Data\layout.toc ← file-system layout
|
||||
Reads Patch\initfs_Win32 ← patches on top of base
|
||||
Reads Documents\FIFA 23\fifasetup.ini ← display settings
|
||||
Reads Data\locale.ini ← language table
|
||||
Reads Data\db_meta.xml (via FLE) ← DB schema for all tables
|
||||
```
|
||||
|
||||
## Phase timing (observed, single machine)
|
||||
|
||||
| Phase | Time after launch | Trigger |
|
||||
|------------------------------|-------------------|----------------------------------|
|
||||
| DLL load + FLE injection | 0 – 0.3 s | OS loader |
|
||||
| Engine + DirectX init | 0.3 – 5 s | FIFA23 entry point |
|
||||
| "Press any key" splash | ~5 s | First rendered frame |
|
||||
| Main menu | ~25 s | After key press |
|
||||
| FUT mode entry (attempted) | user-driven | User selects FUT tile |
|
||||
| Network calls to EA services | at FUT entry | DirtySDK / EAWebKit |
|
||||
|
||||
## Files read at startup (observed)
|
||||
|
||||
| File | Format | Purpose |
|
||||
|------|--------|---------|
|
||||
| `Data/initfs_Win32` | Frostbite pkg | Base asset manifest |
|
||||
| `Data/layout.toc` | Frostbite TOC | File layout index |
|
||||
| `Patch/initfs_Win32` | Frostbite pkg | Patch layer |
|
||||
| `Data/locale.ini` | INI | String localisation |
|
||||
| `Data/db_meta.xml` | XML | DB schema (loaded by FLE) |
|
||||
| `Data/id_map.json` | JSON | Player/team ID→name map |
|
||||
| `Data/char_conv.json` | JSON | Character conversion table |
|
||||
| `Documents/FIFA 23/fifasetup.ini` | INI | Display/audio settings |
|
||||
| `AppData/Local/Temp/FIFA 23/_replay0.bin` | binary | Replay buffer |
|
||||
| `anadius.cfg` | VDF | Fake EA persona config |
|
||||
| `AppData/Local/anadius/LSX emu/achievement-*.xml` | XML | Achievement state |
|
||||
|
||||
## Files written during a session (observed)
|
||||
|
||||
| File | When written | Content |
|
||||
|------|-------------|---------|
|
||||
| `Documents/FIFA 23/settings/Settings*` | Main menu reached | FBCHUNKS — controller/display prefs |
|
||||
| `Documents/FIFA 23/settings/ProfileOptions` | Profile load | FBCHUNKS — 1.5 MB profile blob |
|
||||
| `Documents/FIFA 23/filesystemcache/survey.state` | Startup | Empty state file |
|
||||
| `Documents/FIFA 23/filesystemcache/atlPlayTimeJson/playtime_*.json` | Ongoing | Playtime tracking |
|
||||
| `FIFA 23 Live Editor/config.json` | FLE ready | FLE settings (rewritten each session) |
|
||||
| `Logs/log_DD-MM-YYYY.txt` | Throughout | FLE debug log |
|
||||
|
||||
## Save file formats
|
||||
|
||||
### FBCHUNKS (Frostbite chunk container)
|
||||
- Magic: `46 42 43 48 55 4E 4B 53` (`FBCHUNKS`)
|
||||
- Byte 8: version (01 seen)
|
||||
- Offset 0x12: null-terminated label string (e.g. "Personal Settings 1", "Career - Player Progress 1")
|
||||
- Remainder: compressed/binary chunk data — no public spec; requires Frostbite tooling to fully parse
|
||||
- Tools: [Frosty Tool Suite](https://github.com/CadeEvs/FrostyToolSuite) can read/write these
|
||||
|
||||
### fifasetup.ini
|
||||
- Plain `KEY = VALUE` ini, fully human-readable
|
||||
- Safe to edit (display resolution, locale, vsync)
|
||||
|
||||
## Network calls at FUT entry (observed with iptables redirect)
|
||||
|
||||
Traffic pattern captured before changing strategy:
|
||||
- Multiple TLS connections to port 443 (destination: EA servers, resolved as various EA IPs)
|
||||
- TLS 1.3, AES-256-GCM (DirtySDK's copy of ProtoSSL, inline in FIFA23.exe)
|
||||
- No SNI sent (DirtySDK does not set `server_name` extension)
|
||||
- Connections originate from Wine/Proton network stack via Linux kernel TCP
|
||||
|
||||
Specific EA hostnames used (from openfut-bridge captures, not decoded from TLS):
|
||||
- `fut.ea.com` (FUT API)
|
||||
- `accounts.ea.com` (auth)
|
||||
- `gateway.ea.com` (entitlements)
|
||||
- `pin-river.data.ea.com` (telemetry)
|
||||
|
||||
## Key FLE Lua API hooks
|
||||
|
||||
FLE injects `FIFALiveEditor.DLL` and exposes a Lua engine that can:
|
||||
- Read any in-memory DB table via `GetDBTableRows(tableName)`
|
||||
- Write any cell via `EditDBTableField`
|
||||
- Query career mode state via `IsInCM()`
|
||||
- Get player/team names via `GetPlayerName`, `GetTeamName`
|
||||
|
||||
This is the primary safe integration path (see `fut-integration-options.md`).
|
||||
@@ -1,191 +0,0 @@
|
||||
# Foundational test — live custom XI via Freeze Lineup
|
||||
|
||||
**Status: PENDING — test has not yet been run.**
|
||||
|
||||
This is build-order step 1 from `docs/direction.md`: the test everything else
|
||||
in the direction pivot depends on.
|
||||
|
||||
## What changed since the first draft of this doc
|
||||
|
||||
The first version of this test guessed at a "selection bias" DB field and a
|
||||
candidate squad/lineup table name, based on general FIFA-modding precedent
|
||||
that turned out not to hold for FLE's documented API — no such field appears
|
||||
anywhere in FLE's actual Lua API docs or its own example scripts. While
|
||||
researching an unrelated hotkey issue, a **confirmed, FLE-documented**
|
||||
mechanism for forcing a starting XI turned up instead: the **Formation
|
||||
Editor's "Freeze Lineup" feature** (FLE wiki, `Formation-Editor.md`):
|
||||
|
||||
> This feature can be used in player career mode if you want to manage the
|
||||
> starting lineup of your team. Can be also used in manager career mode to
|
||||
> manually manage your next opponent's starting lineup.
|
||||
|
||||
Steps (GUI, no scripting): open Formation Editor for a team → arrange players
|
||||
on the pitch → tick **Freeze Lineup** → `Data → Save`.
|
||||
|
||||
This is real and documented, but it's GUI-only — there is no Lua function for
|
||||
it, and what DB write it actually performs under the hood is undocumented.
|
||||
This test is now two phases: confirm the GUI feature works at all, then
|
||||
reverse the DB write it makes so it can be replicated programmatically
|
||||
(required for the app→game bridge in build-order step 2, which needs this
|
||||
driven from outside the game, not from a person clicking checkboxes).
|
||||
|
||||
Also fixed in this pass: `EditDBTableField`'s real signature, confirmed from
|
||||
FLE's own docs and `lua/scripts/99ovr_99pot.lua`, is
|
||||
`EditDBTableField(cell)` where `cell` is `row["fieldname"]` with `.value`
|
||||
mutated in place — **not** `EditDBTableField(table, row_index, field, value)`
|
||||
as originally (incorrectly) written into the first draft of the injector
|
||||
script.
|
||||
|
||||
## What this test settles
|
||||
|
||||
Whether a *specific, externally-chosen* 11 players can be forced into a
|
||||
career (or Kick-Off) match's starting lineup, live, with no restart — and
|
||||
whether the mechanism that does it (Freeze Lineup's underlying DB write) can
|
||||
be driven by a script instead of a person clicking through the Formation
|
||||
Editor UI.
|
||||
|
||||
If Freeze Lineup itself doesn't actually hold under match start (the wiki
|
||||
doesn't show it being tested against a live match, only "you should be able
|
||||
to see... when you play against them"), the whole bridge architecture in
|
||||
`docs/direction.md` §3 needs rethinking — there is no other documented
|
||||
mechanism for forcing a lineup.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- FIFA 23 launched normally (FLE injected, EAAC neutralized — same baseline
|
||||
as `track-c-fut-table-test.md`)
|
||||
- A career save loaded (Freeze Lineup is documented for career mode
|
||||
specifically — confirm separately whether it does anything in Kick-Off,
|
||||
don't assume it does)
|
||||
- Note 11 player IDs from your club (`tools/squad-exporter/export_squad.lua`
|
||||
output, `playerid` field) that are NOT currently your starting XI
|
||||
|
||||
## Phase 1 — confirm Freeze Lineup actually holds into a match
|
||||
|
||||
This has zero scripting and should be done first since everything else is
|
||||
wasted effort if it fails.
|
||||
|
||||
1. Open the Live Editor overlay (F9, or `Windows → Settings` from the
|
||||
overlay's own menu bar if the hotkey isn't registering — see the umu/Wine
|
||||
hotkey note below).
|
||||
2. `Features → Teams` → find your team → `Edit`.
|
||||
3. `Team → Formation` to open the Formation Editor.
|
||||
4. Swap players around on the pitch so the XI differs from your current
|
||||
actual starting XI in some checkable way (e.g. swap two outfield players'
|
||||
positions, or bench/start a specific player).
|
||||
5. Tick **Freeze Lineup**.
|
||||
6. `Data → Save`.
|
||||
7. Hide Live Editor (F9), save your career **on a new slot** (don't overwrite
|
||||
your main save in case this corrupts something), exit to main menu, reload
|
||||
that save, and check the team's lineup screen / play a match and watch who
|
||||
starts.
|
||||
|
||||
**Record in the Results table below whether the frozen lineup actually took
|
||||
the pitch.** If not, stop here — Phase 2 is moot.
|
||||
|
||||
## Phase 2 — find the underlying DB write
|
||||
|
||||
Only proceed if Phase 1 confirmed Freeze Lineup works.
|
||||
|
||||
1. In FLE's Lua Engine, run `tools/squad-injector/snapshot_lineup_tables.lua`.
|
||||
This dumps every DB table whose name contains `squad`, `lineup`,
|
||||
`formation`, `tactic`, `teamsheet`, `selection`, `players`, or `teams` to
|
||||
`C:\FIFA 23 Live Editor\openfut_snapshot_<timestamp>.json`. Note this
|
||||
filename — this is your **before** snapshot.
|
||||
2. Without restarting or reloading, repeat the Formation Editor steps from
|
||||
Phase 1 (steps 2–6 only — open Formation Editor, change the lineup, tick
|
||||
Freeze Lineup, `Data → Save`). Don't save/reload the career between
|
||||
snapshot and this step — keep it to a single live session so the diff
|
||||
isn't polluted by other state changes.
|
||||
3. Run `snapshot_lineup_tables.lua` again. This is your **after** snapshot.
|
||||
4. Copy both JSON files out of the Wine prefix (same path pattern as
|
||||
`track-c-fut-table-test.md`: `~/Games/umu/.../drive_c/FIFA 23 Live
|
||||
Editor/`) and run:
|
||||
|
||||
```bash
|
||||
python3 tools/squad-injector/diff_snapshots.py before.json after.json
|
||||
```
|
||||
|
||||
5. The output shows exactly which table(s) and field(s) changed. This is the
|
||||
real, confirmed write Freeze Lineup performs — record it in the Results
|
||||
table below.
|
||||
|
||||
## Phase 3 — replicate the write via script
|
||||
|
||||
1. Open `tools/squad-injector/apply_lineup_write.lua` and fill in
|
||||
`TARGET_TABLE` and `TARGET_FIELDS` using Phase 2's diff output.
|
||||
2. Edit `C:\FIFA 23 Live Editor\openfut_test_xi.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"team_id": 12345,
|
||||
"xi": [
|
||||
{ "player_id": 111111, "position": 0 },
|
||||
{ "player_id": 222222, "position": 5 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Use 11 entries. Position codes are **confirmed numeric 0–27**
|
||||
(`GK=0, SW=1, RWB=2, RB=3, RCB=4, CB=5, LCB=6, LB=7, LWB=8, RDM=9, CDM=10,
|
||||
LDM=11, RM=12, RCM=13, CM=14, LCM=15, LM=16, RAM=17, CAM=18, LAM=19,
|
||||
RF=20, CF=21, LF=22, RW=23, RS=24, ST=25, LS=26, LW=27`) — from
|
||||
`lua/scripts/export_season_stats.lua`'s `get_pos_name` table in FLE's own
|
||||
repo, not a guess.
|
||||
3. Run `apply_lineup_write.lua` from FLE's Lua Engine.
|
||||
4. Repeat the save-to-new-slot / reload / check-lineup verification from
|
||||
Phase 1, but this time without ever opening the Formation Editor — the
|
||||
write was made entirely from the script.
|
||||
|
||||
## Classification criteria
|
||||
|
||||
### "Confirmed — full mechanism works"
|
||||
|
||||
Phase 1 holds, Phase 2 finds a clean diff, Phase 3's scripted write produces
|
||||
the same in-match result as the manual GUI path.
|
||||
|
||||
**Verdict:** Build-order step 1 done. Proceed to step 2 (bridge transport) in
|
||||
`docs/direction.md`.
|
||||
|
||||
### "GUI works, script doesn't"
|
||||
|
||||
Phase 1 holds but Phase 3's replicated write doesn't stick, even though the
|
||||
diffed fields matched what changed in Phase 2.
|
||||
|
||||
**Verdict:** Freeze Lineup likely does more than a single DB field write
|
||||
(e.g. an internal engine call beyond `EditDBTableField`'s reach, or a second
|
||||
write the diff missed because it happened in a table outside the `KEYWORDS`
|
||||
filter in `snapshot_lineup_tables.lua` — widen the filter and redo Phase 2).
|
||||
|
||||
### "Freeze Lineup doesn't hold at all"
|
||||
|
||||
Phase 1 fails — the lineup reverts to the game's own AI-picked XI regardless.
|
||||
|
||||
**Verdict:** No confirmed mechanism exists for forcing a lineup. This kills
|
||||
the bridge architecture as designed in `direction.md` §3 and needs a return
|
||||
to first principles — there is no fallback documented anywhere in FLE's wiki
|
||||
for this specific case.
|
||||
|
||||
## A note on the umu/Wine F9/F11 hotkey issue
|
||||
|
||||
If FLE's F9 (hide/show) hotkey isn't registering under umu, this is plausibly
|
||||
a Wine keyboard-hook limitation (FLE's global hotkey detection likely uses a
|
||||
low-level hook that doesn't translate cleanly through Wine's input layer) —
|
||||
not something documented anywhere in FLE's own troubleshooting docs, which
|
||||
don't mention Linux/Wine at all. F11 specifically has **no documented FLE
|
||||
function** — F9 is the only documented toggle. Workaround: click directly
|
||||
into the FLE overlay window (it should still be visible/clickable even if the
|
||||
hotkey doesn't fire) and use its own menu bar instead of relying on the
|
||||
hotkey.
|
||||
|
||||
## Results
|
||||
|
||||
*(To be filled in after the test is run.)*
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Date run | — |
|
||||
| Phase 1: Freeze Lineup holds into a match? | — |
|
||||
| Phase 2: table(s)/field(s) changed | — |
|
||||
| Phase 3: scripted write reproduces Phase 1 result? | — |
|
||||
| **Classification** | **PENDING** |
|
||||
@@ -1,136 +0,0 @@
|
||||
# FUT Integration Options
|
||||
|
||||
How to connect FIFA 23 to the OpenFUT local simulator, ranked by safety and feasibility.
|
||||
|
||||
## Option A — FLE Lua scripting (RECOMMENDED)
|
||||
|
||||
**What it does:** Use FIFA Live Editor's in-memory Lua API to read and write the game's
|
||||
database tables at runtime. FLE is already injected; no additional hooking needed.
|
||||
|
||||
**Why it's the right path:**
|
||||
- Fully offline, no EA servers touched
|
||||
- FLE is already trusted by the user (it's the launch mechanism)
|
||||
- `GetDBTableRows` / `EditDBTableField` expose the full Frostbite DB in memory
|
||||
- Scripts run inside the game process; no IPC complexity
|
||||
- Same mechanism used by modders for career mode edits today
|
||||
|
||||
**Integration design:**
|
||||
|
||||
```
|
||||
openfut-core (SQLite)
|
||||
│
|
||||
│ HTTP REST (localhost)
|
||||
▼
|
||||
openfut-bridge (port 8080, plain HTTP, no TLS)
|
||||
│ pulls club/squad/player data as JSON
|
||||
▼
|
||||
FLE Lua bridge script
|
||||
│ calls GetDBTableRows, EditDBTableField
|
||||
▼
|
||||
FIFA 23 in-memory DB (Frostbite)
|
||||
```
|
||||
|
||||
The Lua script polls openfut-core's REST API at intervals (or on FUT menu entry)
|
||||
and writes simulator data (coins, items, squad) into the appropriate DB tables.
|
||||
|
||||
**Tables likely involved (to verify with export_squad.lua):**
|
||||
|
||||
| Table | Expected FUT content |
|
||||
|-------|---------------------|
|
||||
| `players` | Player attributes (OVR, potential, stats) |
|
||||
| `teams` | Club identity, stadium, colors |
|
||||
| `fut_clubs` | FUT club record (if in memory when FUT loads) |
|
||||
| `fut_items` | Card inventory (if in memory) |
|
||||
| `fut_squads` | Active squad (if in memory) |
|
||||
|
||||
**Steps to implement:**
|
||||
1. Run `tools/squad-exporter/export_squad.lua` from FLE Lua Engine while in FUT to discover which tables are live
|
||||
2. Map openfut-core's data model to the discovered table fields
|
||||
3. Write a Lua polling script that fetches `/api/v1/club`, `/api/v1/squad`, etc. from openfut-core and calls `EditDBTableField` to populate them
|
||||
4. Optionally add a small HTTP client to the Lua script using LuaSocket (FLE ships with Lua 5.4)
|
||||
|
||||
**Limitations:**
|
||||
- Changes are in-memory only; they reset on game restart (acceptable for a simulator)
|
||||
- Only works while FLE is running (always true in our setup)
|
||||
- FUT tables may only be populated when the FUT hub is loaded; test with the exporter
|
||||
|
||||
---
|
||||
|
||||
## Option B — Local save file injection (career mode proxy)
|
||||
|
||||
**What it does:** Generate or modify offline career mode save files that contain FUT-like
|
||||
squad/player data, using Frostbite's FBCHUNKS format.
|
||||
|
||||
**Feasibility:** Medium
|
||||
- FBCHUNKS format is not publicly documented but has been partially reverse-engineered by the Frosty Tool Suite project
|
||||
- Career saves are 16 MB — large and complex
|
||||
- Changes take effect only after a game restart
|
||||
|
||||
**Best use:** Pre-populating a career club with the same players as the FUT simulator squad, so offline Squad Battles use "your" players.
|
||||
|
||||
**Steps:**
|
||||
1. Use Frosty Tool Suite to open a career save and map the schema
|
||||
2. Build a Python exporter that writes a valid FBCHUNKS save with simulator squad data
|
||||
3. Test: replace the career save, launch FIFA, verify squad is correct
|
||||
|
||||
---
|
||||
|
||||
## Option C — Local companion web UI
|
||||
|
||||
**What it does:** The user manages their FUT simulator entirely in a web browser (openfut-core already has this). A button exports the current squad/club state to a format that a Lua script or file injector can consume.
|
||||
|
||||
**This is already implemented** — openfut-core serves the FUT simulator REST API. The missing piece is the Lua bridge script (Option A) that reads from it.
|
||||
|
||||
---
|
||||
|
||||
## Option D — Local proxy for non-secured local calls only
|
||||
|
||||
**What it does:** Intercept FIFA 23's calls to `localhost:*` or a known local endpoint (not EA servers) and respond with simulator data.
|
||||
|
||||
**Feasibility:** Low value in isolation
|
||||
- FIFA 23 does not make calls to localhost in normal operation (except EA App on port 10853)
|
||||
- All FUT API calls go to EA's servers over TLS
|
||||
- Intercepting those would require the approach we explicitly ruled out
|
||||
|
||||
**Not recommended as a primary path.** Could be combined with Option A if the Lua script exposes a local socket that a coordinator process writes to.
|
||||
|
||||
---
|
||||
|
||||
## Option E — Memory bridge (Cheat Engine / FLE offsets)
|
||||
|
||||
**What it does:** Use known memory offsets (FLE's `offset_cache.json`) to read/write FUT state directly in FIFA23.exe's heap.
|
||||
|
||||
**Feasibility:** Medium — FLE already does this for career mode
|
||||
- FLE's `offset_cache.json` contains addresses for many game structures
|
||||
- FUT in-memory structs are separate from career structs and may not be mapped yet
|
||||
- This is fragile (offsets change with game updates)
|
||||
|
||||
**Not recommended** unless Options A and B both fail — too brittle.
|
||||
|
||||
---
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Start with Option A (FLE Lua scripting).**
|
||||
|
||||
1. Run `tools/squad-exporter/export_squad.lua` in-game to discover which DB tables exist in FUT mode
|
||||
2. Use `tools/file-watch-diff/watch.sh` to snapshot file state entering FUT and identify any new local files
|
||||
3. Use `tools/network-metadata-logger/netlog.sh` to log which EA hosts FIFA contacts at FUT entry (metadata only, no decryption)
|
||||
4. Map findings back to openfut-core's data model
|
||||
5. Implement the Lua bridge script that calls openfut-core's REST API and writes to discovered tables
|
||||
|
||||
If FUT tables are not exposed by FLE's DB API (they may not be — FUT data lives server-side in online mode), fall back to **Option B** (career save injection) to provide a squad that mirrors the simulator's club.
|
||||
|
||||
---
|
||||
|
||||
## Safety boundary
|
||||
|
||||
The following are out of scope and must not be implemented:
|
||||
|
||||
- Decrypting or inspecting EA's TLS traffic
|
||||
- Spoofing EA domain names or impersonating EA servers
|
||||
- Sending modified clients to EA's production services
|
||||
- Bypassing EA App login or account verification
|
||||
- Anything that could constitute online cheating or violate EA's ToS for online play
|
||||
|
||||
All integration must remain local/offline/single-player.
|
||||
@@ -1,523 +0,0 @@
|
||||
# FIFA 17 — Clean Empty-My-Packs client fix (DESIGN / RESEARCH ONLY)
|
||||
|
||||
Status: **design only — no client binary/movie changes made.** This is the client-side
|
||||
follow-up to bug 6c. The backend already ships a compatibility workaround (P2, active
|
||||
non-openable sentinel 65534; see `docs/evidence/STORE_TILE_6C.md` §17 and
|
||||
`FIFA17_EMPTY_MYPACKS_CLIENT_CONTRACT.md`). This plan describes what a *client-side*
|
||||
fix would need to change so the backend shim can eventually become unnecessary for
|
||||
patched clients.
|
||||
|
||||
Do NOT patch the executable, DLLs, or Scaleform movies in this task.
|
||||
|
||||
## 1. Established client-side evidence
|
||||
|
||||
Binary: `CardsDLL_Win64_retail.dll`
|
||||
SHA-256 `4706a881ae1fc7b5769fd810b25a868d29d2b16a8e65a7513436327ef645573c`
|
||||
(dump load base `0x00006FFFFC120000`; RE-space base `0x180000000`). `FIFA17.exe`
|
||||
(`29c31cef…`) is Denuvo-packed (decrypts only in live memory).
|
||||
|
||||
Store category pipeline (all decompiled; see `docs/plan-2026-08-05-store-subsystem.md`):
|
||||
- `FUN_1800150d0` — builds display groups from `purchase[]`; a `mypacks` group exists
|
||||
iff some pack has `displayGroup.value=="mypacks"`. `group+0x104=(value=="mypacks")`,
|
||||
tiles in `group+0x40`, ordinal in `group+0x00` (1-based creation order).
|
||||
- `FUN_18007dab0` → `FUN_1800147f0(model, screen+0x290, …)` — renders/resolves a
|
||||
category. `screen+0x290==0` lists group tiles (`FUN_180014610`); otherwise
|
||||
`FUN_180014420` exact-matches the ordinal and **returns NULL on a miss**, after
|
||||
which `FUN_1800147f0` dereferences `[RAX+0x48]` with **no null guard** →
|
||||
**crash at `0x180014882`** (`ACCESS_VIOLATION` read of `0x48`, minidump-confirmed).
|
||||
- `screen+0x290` is written in exactly two CardsDLL sites: ctor `FUN_18007d1a0`
|
||||
writes `0`; **`FUN_18007e7f0` case `0x7551` copies the Flash movie message field
|
||||
`CATEGORY_ID` verbatim** into it. So the category is chosen by the Scaleform movie.
|
||||
- `FUN_18007e5e0` binds the six store tabs (`FUN_180014580`: `mypacks, points, bronze,
|
||||
silver, gold, special`) to `PANEL_ID` = matching group ordinal, or hides the panel.
|
||||
- Unopened-pack count signals (server, already correct at 0 when empty):
|
||||
`userInfo.unopenedPacks.recoveredPacks` and `/user/credits .unopenedPacks`. The hub
|
||||
`CentralUnclaimedPack` tile (destination `GOTO_STORE_MYPACK`) is gated by this count
|
||||
in the hub model (`model+0x20950`). **Candidate F (a server count gating the STORE's
|
||||
My-Packs resolution) was CONTRADICTED**: the count is correct at 0 yet the store
|
||||
still resolves My Packs, because the decision is movie-side.
|
||||
|
||||
## 2. Desired clean client behavior
|
||||
|
||||
```
|
||||
unopened-pack count == 0:
|
||||
Store defaults to Browse Packs (e.g. a real category such as bronze/gold)
|
||||
My Packs is NOT selected/resolved
|
||||
no synthetic placeholder tile is required from the server
|
||||
unopened-pack count > 0:
|
||||
existing My Packs behavior unchanged
|
||||
```
|
||||
|
||||
## 3. Candidate insertion points (ranked)
|
||||
|
||||
Ranking favors fixing the UX (not merely preventing the crash) and the smallest,
|
||||
lowest-risk change that achieves it.
|
||||
|
||||
### Rank 1 (preferred, best UX) — Scaleform / category-selection layer
|
||||
Prevent the movie from emitting `CATEGORY_ID == mypacks` (and from defaulting the
|
||||
store into My Packs) when the unopened-pack count is 0; default to Browse Packs
|
||||
instead.
|
||||
- **Where:** the FUT Store Scaleform movie / ActionScript (`StoreFront`,
|
||||
`CATEGORY_ID`/`ACTION_GET_PACKLIST`, `GOTO_STORE_MYPACK`), which the packed exe hosts
|
||||
and which reads the hub model (it already knows the count for the
|
||||
`CentralUnclaimedPack` tile).
|
||||
- **Behavior changed:** the store's initial/selected category when empty.
|
||||
- **Scope:** movie asset edit (client-side), no native-code patch.
|
||||
- **Risk:** medium — Scaleform RE/editing is fiddly; must find where the default
|
||||
`CATEGORY_ID` is chosen and gate it on the count without breaking the count>0 path.
|
||||
- **Compatibility:** per-client asset change; does not touch protocol or other clients.
|
||||
- **Fixes UX or just crash?** **UX** — no fake tile, correct default; the crash also
|
||||
disappears because `mypacks` is never resolved when absent.
|
||||
- **Evidence:** `screen+0x290 ← CATEGORY_ID` (`FUN_18007e7f0` case `0x7551`); count
|
||||
already available client-side (hub model / `unopenedPacks`).
|
||||
|
||||
### Rank 2 — Native Store resolver fallback (CardsDLL)
|
||||
Make `FUN_1800147f0`/`FUN_180014420` fall back to a safe category (e.g. list-tiles
|
||||
`N==0`, or the first existing group) when the requested ordinal misses, instead of
|
||||
dereferencing NULL.
|
||||
- **Behavior changed:** category-miss handling for ALL categories, not just mypacks.
|
||||
- **Scope:** small, localized CardsDLL binary patch near `0x180014420`/`0x180014882`.
|
||||
- **Risk:** medium — alters native store behavior globally; could mask other
|
||||
legitimate misses; the movie may still believe it is in My Packs (empty/odd view).
|
||||
- **Compatibility:** binary patch to the shipped DLL (client-side).
|
||||
- **Fixes UX or just crash?** Crash + partial UX (no crash, but the empty-My-Packs
|
||||
view may still be awkward).
|
||||
- **Evidence:** the no-guard deref at `0x180014882`; `FUN_180014420` returns NULL on
|
||||
miss.
|
||||
|
||||
### Rank 3 (cheapest, crash-only) — CardsDLL null guard
|
||||
Insert a null check before the `[RAX+0x48]` dereference in `FUN_1800147f0` (a single
|
||||
`TEST/JZ` around the deref) so a NULL group is skipped/returned safely.
|
||||
- **Behavior changed:** only the crash path.
|
||||
- **Scope:** minimal (a few bytes) binary patch at `~0x180014882`.
|
||||
- **Risk:** low — smallest change; but purely crash-prevention. With no `mypacks`
|
||||
group the resulting empty view is unverified (could be a blank/empty-category state).
|
||||
- **Compatibility:** binary patch (client-side).
|
||||
- **Fixes UX or just crash?** Crash only.
|
||||
- **Evidence:** minidump faulting instruction `CardsDLL+0x14882`, `[NULL+0x48]`.
|
||||
|
||||
## 4. Recommended long-term outcome
|
||||
|
||||
Rank 1 (Scaleform default-category gating) is the clean fix: with count 0 the store
|
||||
opens on Browse Packs, no `mypacks` resolution, no fake tile — and the **backend
|
||||
sentinel 65534 can be dropped for patched clients** (the server would simply omit the
|
||||
`mypacks` group when empty, which is safe once the client no longer resolves it).
|
||||
Rank 3 (null guard) is a cheap universal crash-safety net that could ship alongside.
|
||||
Until a client-side fix exists, the backend P2 sentinel remains the required
|
||||
compatibility behavior for unpatched retail clients.
|
||||
|
||||
## 5. Open questions / next research (no execution here)
|
||||
- Locate the Store movie's default/initial `CATEGORY_ID` selection and confirm it can
|
||||
read the unopened count (Rank 1 feasibility).
|
||||
- Confirm, via a guarded-resolver experiment, what the empty-My-Packs view degrades to
|
||||
if the `mypacks` group is simply absent + a null guard is present (Rank 2/3).
|
||||
- Determine whether the Browse-Packs→My-Packs navigation gate (observed with the
|
||||
active sentinel) also resolves under Rank 1.
|
||||
|
||||
---
|
||||
|
||||
# PART II — Native client-fix design (RE-backed, 2026-08-13)
|
||||
|
||||
Investigation-and-design phase (no client binary/movie changed, no backend changed,
|
||||
no new live Store experiment). CardsDLL was re-analysed in Ghidra on `.105`; the
|
||||
in-repo decompiled addresses were reconfirmed against a freshly-built project. Every
|
||||
claim below is labelled **ESTABLISHED** (read from this build's binary / crash dump),
|
||||
**PROPOSED** (design, not yet implemented), or **UNKNOWN**.
|
||||
|
||||
## 6. Binary + environment verification (ESTABLISHED)
|
||||
|
||||
Hashes re-verified on `.105` (`/mnt/games/FIFA 17/`) — identical to the recorded RE:
|
||||
- `CardsDLL_Win64_retail.dll` SHA-256 `4706a881ae1fc7b5769fd810b25a868d29d2b16a8e65a7513436327ef645573c`,
|
||||
size 3179952, PE `TimeDateStamp` 1497050156 (2017-06-09T23:15:56Z), `SizeOfImage`
|
||||
`0x31d000`, image base `0x180000000` (RE-space). **Unpacked → statically analysable.**
|
||||
- `FIFA17.exe` SHA-256 `29c31cef12b0c3c2a7305220617c7b4fa139ab76b8c857851bdbe88987962899`,
|
||||
size 224639408, **Denuvo-packed** → the Scaleform/StoreFront ActionScript that
|
||||
*decides* to emit `CATEGORY_ID` is NOT statically readable. This is why a
|
||||
pure-Scaleform edit (old "Rank 1") is not the practical vehicle; the fix is taken
|
||||
at the readable native boundary in CardsDLL instead.
|
||||
- Ghidra project rebuilt at `.105:/tmp/ghidra_fut/cardsdll` (headless import+analysis
|
||||
succeeded). Tooling: `fifa17-recon/tools/ghidra_env.py` run under `~/.venv`
|
||||
(`PYTHONPATH=/opt/ghidra/Ghidra/Features/PyGhidra/pypkg/src:/usr/lib/python3.14/site-packages`;
|
||||
`jpype1` reinstalled offline from pip cache). RVAs below = static VA − `0x180000000`.
|
||||
|
||||
## 7. Category-selection path (ESTABLISHED — decompiled this build)
|
||||
|
||||
Store message dispatch `FUN_18007d880` (RVA `0x7d880`) routes Flash message ids:
|
||||
`0x753f → FUN_18007dab0` (render), `0x278a → FUN_18007df60` (publish category ids),
|
||||
and the input handler `FUN_18007e7f0` (RVA `0x7e7f0`) case **`0x7551`** copies the
|
||||
movie field `CATEGORY_ID` verbatim into `screen+0x290` (the only non-ctor writer;
|
||||
ctor `FUN_18007d1a0` writes 0).
|
||||
|
||||
**Store render `FUN_18007dab0` (RVA `0x7dab0`), decompiled verbatim, is the decision
|
||||
point:**
|
||||
```c
|
||||
iVar1 = *(int *)(param_1 + 0x290); // requested CATEGORY_ID (screen+0x290)
|
||||
iVar6 = FUN_180014580(store, 1); // the *points* category id (see tab map)
|
||||
if (iVar1 == iVar6) { // requested category is POINTS (real-money)
|
||||
if (region_check() == 0) { post "REGION_MISMATCH"; return; }
|
||||
if (FUN_180014de0(store) != 0) return; // points group present → handled
|
||||
FUN_180014b60(store, dp); // else points render
|
||||
} else {
|
||||
FUN_1800147f0(store, iVar1, dp, 0, 0); // EVERY other category, incl. My Packs
|
||||
}
|
||||
```
|
||||
- `param_1` (RCX) = the store-screen object; `+0x290` is the requested category.
|
||||
- **Tab→id map `FUN_180014580(store, n)` (RVA `0x14580`): `0=mypacks, 1=points,
|
||||
2=bronze, 3=silver, 4=gold, 5=special`.** Each returns the group's **1-based
|
||||
ordinal** (via caption compare `FUN_180014380`) or **`-1`** if that group is absent.
|
||||
So category ids are DYNAMIC ordinals, not fixed constants. The tab publisher
|
||||
`FUN_18007df60` pushes `MYPACK_/BRONZE_/…_CATEGORY_ID` to the movie from these
|
||||
lookups; the movie echoes one back as `CATEGORY_ID`.
|
||||
- The **points** tab is the only one special-cased (commerce/region gate). **My Packs
|
||||
is NOT special-cased — it falls into the `else` and is resolved by
|
||||
`FUN_1800147f0`.**
|
||||
|
||||
**Resolver `FUN_1800147f0` (RVA `0x147f0`) — the crash (ESTABLISHED, instruction
|
||||
level):**
|
||||
```
|
||||
0x14856: 85 ff TEST EDI,EDI ; EDI = category ordinal (param_2)
|
||||
0x14858: 75 0f JNZ 0x14869 ; ==0 → list-all (Browse), else resolve
|
||||
0x1485a: … CALL 0x14610 ; FUN_180014610 list ALL group tiles
|
||||
0x14867: eb 29 JMP 0x14892
|
||||
0x14869: 8b d7 MOV EDX,EDI
|
||||
0x1486b: e8 … CALL 0x14420 ; FUN_180014420(store, ordinal) → RAX (group|NULL)
|
||||
0x14870: 48 8d 50 40 LEA RDX,[RAX + 0x40] ; RDX = group+0x40 (=0x40 when RAX=NULL)
|
||||
0x14878: 48 3b c2 CMP RAX,RDX
|
||||
0x1487b: 74 15 JZ 0x14892
|
||||
0x14882: 4c 8b 42 08 MOV R8,[RDX + 0x8] ; <-- FAULT: read [0x40+0x8]=0x48 when NULL
|
||||
0x14886: 48 8b 12 MOV RDX,[RDX] ; [0x40]
|
||||
```
|
||||
`FUN_180014420` (RVA `0x14420`) exact-matches `group+0x00` (ordinal), stride `0x108`,
|
||||
**returns NULL on a miss, with no guard in the caller** → faulting read of VA `0x48`
|
||||
at `0x180014882`. This is byte-for-byte the Experiment-B minidump
|
||||
(`0xC0000005` READ `0x48` at `CardsDLL+0x14882`).
|
||||
- `param_2 == 0` → `FUN_180014610` lists **all** group tiles = the safe "Browse Packs"
|
||||
view. `param_2 == existing ordinal` → resolves. `param_2 == a non-existent ordinal`
|
||||
(e.g. `-1`, which `MYPACK_CATEGORY_ID` becomes when the group is absent) → NULL → crash.
|
||||
|
||||
**Why it crashes with zero packs (ESTABLISHED):** with `unopenedPackIds==[]` and no
|
||||
sentinel, no `mypacks` group exists, so `FUN_180014580(store,0) = -1`,
|
||||
`MYPACK_CATEGORY_ID = -1`, the movie still selects My Packs and echoes `CATEGORY_ID =
|
||||
-1`, and `FUN_1800147f0(store, -1, …)` → `FUN_180014420(-1)=NULL` → crash. The active
|
||||
sentinel (65534) works only because it makes a real `mypacks` ordinal exist to resolve.
|
||||
|
||||
## 8. Zero-pack state client-side (ESTABLISHED)
|
||||
|
||||
The client already holds the correct unopened-pack count in a **data-manager
|
||||
singleton** (the same one the store resolver uses):
|
||||
- Obtain: `seed = FUN_1800d7170()` then `FUN_180009c80(&p, seed)` → `p` (release with
|
||||
`p->vtbl[0x08](p)`). This exact accessor already runs inside `FUN_180014420` and
|
||||
`FUN_1800147f0`, so any store-category hook can reach it.
|
||||
- **Read count: `p->vtbl[0x4d8](p)` → int. Write: `p->vtbl[0x4e0](p, n)`.** Confirmed
|
||||
in `FUN_180019780`, which reads slot `0x4d8`, adds the number of set booleans in a
|
||||
pack response, and writes slot `0x4e0` (it also fetches `FutGetPurchasedItems`).
|
||||
- Representation: plain `int`; **0 = no unopened packs**, `>0` = count. Lifetime: the
|
||||
singleton persists for the session; updated on pack acquire/open.
|
||||
- No dedicated "hasUnopenedPacks" boolean helper was found; `count != 0` is the
|
||||
predicate. (The hub `CentralUnclaimedPack` tile is gated by this same count via
|
||||
`model+0x20950`, written by `FUN_18010cdc0`/`FUN_18011e120` — the hub mirror, not the
|
||||
store gate.)
|
||||
|
||||
## 9. Implementation vehicle (ESTABLISHED — reuse, do not build a new loader)
|
||||
|
||||
OpenFUT **already ships a client hook framework**: `openfut-launcher/openfut-hook`
|
||||
(`crate-type=["cdylib"]`) builds **`version.dll`**, a proxy DLL placed in the game dir
|
||||
(`/mnt/games/FIFA 17/version.dll`, present & active; log `~/.wine/drive_c/openfut_hook.log`).
|
||||
- Load path: Wine/Windows loads `version.dll` from the app dir at process start →
|
||||
`DllMain(DLL_PROCESS_ATTACH)` → `install_hooks()`.
|
||||
- Existing hooks (`lib.rs`): `getaddrinfo` (IAT via `iat::resolve`), `connect`
|
||||
(inline detour), `WSAConnect`, `WSAIoctl`/ConnectEx, origin_spy registry/mutex,
|
||||
crypt32 `CertVerifyCertificateChainPolicy`, **and in-memory byte-patching of the
|
||||
loaded (packed) main exe + EAWebKit** (`ssl_patch`: `GetModuleHandleA` → scan for a
|
||||
unique prologue → `VirtualProtect`+`copy_nonoverlapping`).
|
||||
- Inline-hook primitive (`connect_hook`): `write_hook(target, dest)` lays a 14-byte
|
||||
`FF 25 00000000 <abs64>` JMP; `restore_original` restores saved bytes
|
||||
(unhook → call real → rehook, avoiding trampoline relocation).
|
||||
- Config: `openfut.cfg` beside the DLL (`host`/ports today; a `store_mypacks_fix`
|
||||
flag would be added there).
|
||||
- **Suitability for the Store fix: direct.** The DLL is in-process with full access
|
||||
to the loaded `CardsDLL_Win64_retail.dll`; the store fix is a NEW module
|
||||
(`store_hook.rs`) installed from `install_hooks`, reusing the `ssl_patch`
|
||||
signature-scan and the `connect_hook` inline-detour patterns. No new loader, no ASI,
|
||||
no separate injector.
|
||||
|
||||
## 10. Three strategies re-evaluated against the RE (Task 4)
|
||||
|
||||
### A. Category-selection redirect — **PREFERRED** (best UX, native, targeted)
|
||||
Hook `FUN_18007dab0` (RVA `0x7dab0`) at entry; before the original runs, redirect a
|
||||
zero-pack My-Packs request to Browse Packs:
|
||||
```
|
||||
cat = *(int*)(store + 0x290)
|
||||
mypacks_id = FUN_180014580(store, 0) // -1 when the group is absent
|
||||
if (cat == mypacks_id) { // movie asked for My Packs (incl. cat==-1==id)
|
||||
if (unopened_count() == 0) // singleton vtbl[0x4d8]
|
||||
*(int*)(store + 0x290) = 0; // 0 = FUN_180014610 list-all = Browse Packs
|
||||
}
|
||||
// then call the original FUN_18007dab0(store)
|
||||
```
|
||||
- Uses the real count? **Yes** (singleton `vtbl[0x4d8]`). Removes the fake 65534 tile?
|
||||
**Yes** (server can omit the group). Removes the click-dialog? **Yes** (no placeholder
|
||||
to click). Removes the Browse→My-Packs nav gate? **Yes** (store lands on Browse, not
|
||||
an empty My-Packs). Preserves count>0? **Yes** (`cat==mypacks_id` with count>0 is left
|
||||
untouched → normal My Packs). Affects other categories? **No** (`cat!=mypacks_id`
|
||||
path is unmodified; points/bronze/… unchanged).
|
||||
- Prevents the crash as a side effect (My Packs is never resolved when its group is
|
||||
absent). This is the old "Rank 1" INTENT, implemented at the readable native boundary
|
||||
instead of in packed Scaleform.
|
||||
|
||||
### B. Resolver fallback — acceptable safety net, less targeted
|
||||
In `FUN_1800147f0` (or right after the `CALL 0x14420` at RVA `0x1486b`): if the
|
||||
resolved group is NULL, fall back to list-all (`param_2=0`) instead of dereferencing.
|
||||
- Prevents crash? **Yes.** Fixes default nav / removes fake tile? **Partially** — the
|
||||
movie still believes it is in My Packs, so the view may be an empty/odd My-Packs
|
||||
rather than a clean Browse. Leaves other lookups unchanged? **It changes miss-handling
|
||||
for ALL categories** — a generic NULL fallback that could mask a genuine
|
||||
missing-category protocol bug. Higher risk than A for that reason; keep as a
|
||||
belt-and-braces guard, not the primary UX fix. The resolver does NOT know *why*
|
||||
`mypacks` is missing, which is exactly the concern the task flags.
|
||||
|
||||
### C. Null-guard only — weakest (crash-only)
|
||||
Insert `TEST RAX,RAX; JZ 0x14892` immediately after `CALL 0x14420` (RVA `0x1486b`),
|
||||
before `LEA RDX,[RAX+0x40]`. Needs a trampoline (no inline slack).
|
||||
- Converts the crash into whatever an empty tile-vector renders (unverified; likely a
|
||||
blank/empty category). Does **not** remove the fake tile or fix the default category;
|
||||
the sentinel would still be needed for acceptable UX. Verified as expected-weakest.
|
||||
|
||||
## 11. Concrete hook target for strategy A (Task 6, PROPOSED)
|
||||
```
|
||||
module: CardsDLL_Win64_retail.dll (GetModuleHandleA)
|
||||
function: FUN_18007dab0 (store render / message 0x753f)
|
||||
RVA: 0x7dab0 (static VA 0x18007dab0)
|
||||
calling conv: Microsoft x64 fastcall; single arg store-screen ptr in RCX
|
||||
screen offset: store+0x290 = requested CATEGORY_ID (int)
|
||||
helpers to call: FUN_180014580 (RVA 0x14580) tab→ordinal, arg0=RCX store, arg1=EDX index(0=mypacks)
|
||||
count singleton: FUN_1800d7170 (0xd7370-seed) + FUN_180009c80 (0x9c80), read vtbl[0x4d8]
|
||||
redirect target: set store+0x290 = 0 (FUN_180014610 list-all → Browse Packs)
|
||||
original behavior: zero packs → resolves absent mypacks ordinal → FUN_180014420 NULL → crash at 0x14882
|
||||
desired behavior: zero packs + mypacks requested → store+0x290 forced to 0 → Browse Packs; no crash/dialog/tile
|
||||
```
|
||||
Hook mechanics (reuse `connect_hook`): lay a 14-byte `FF 25` JMP at `base+0x7dab0` to a
|
||||
Rust `hooked_store_render(store)`; inside: apply the redirect, unhook, call real
|
||||
`FUN_18007dab0(store)`, rehook, return its value. Intercepting only the entry means the
|
||||
minimum interception is the 14 JMP bytes; the first instructions of `FUN_18007dab0`
|
||||
(`MOV RAX,RSP; MOV [RAX+8],RCX; PUSH …`) are a standard prologue safe to save/restore.
|
||||
Alt insertion point (earlier): `FUN_18007e7f0` case `0x7551`, where `CATEGORY_ID` is
|
||||
written to `screen+0x290` — redirect there instead of at render. Entry-hook of
|
||||
`FUN_18007dab0` is preferred (single, well-typed arg; runs once per store render).
|
||||
|
||||
Thread/context: the store screen runs on the client's UI/update thread; the hook reads
|
||||
one int and (rarely) writes one int on the same object the callee immediately reads —
|
||||
no new synchronization needed. Called for categories other than My Packs? The FUNCTION
|
||||
is, but the redirect body only fires when `cat==mypacks_id`, so other tabs are
|
||||
untouched.
|
||||
|
||||
## 12. Version / build safety (Task 7, PROPOSED)
|
||||
FIFA17-specific compat code MUST validate the client before hooking, and MUST no-op on
|
||||
any other build (the same `version.dll` is also used for FIFA23):
|
||||
1. **Module gate:** only proceed if `GetModuleHandleA("CardsDLL_Win64_retail.dll")`
|
||||
resolves (FIFA23 has no such module → auto-skip).
|
||||
2. **Build gate (both, belt-and-braces):**
|
||||
- Exact hash/PE gate: on-disk SHA-256 == `4706a881…`, or PE `SizeOfImage==0x31d000`
|
||||
&& `TimeDateStamp==1497050156` (cheap in-memory check).
|
||||
- Signature scan + validation: locate `FUN_18007dab0` by a unique prologue/byte
|
||||
window rather than trusting the RVA, and assert the known bytes at the branch
|
||||
(`85 ff 75 0f` region) and at the resolver `CALL 0x14420` site match before
|
||||
installing. Recommend **both**: hash to reject the wrong game fast, signature to
|
||||
confirm the exact patch site.
|
||||
3. **Failure behavior:** any check fails (unknown/updated build) → **do NOT patch**,
|
||||
log, and leave the **backend P2 active-sentinel (65534) as the fallback**. Never
|
||||
patch or crash an unrecognised build.
|
||||
|
||||
## 13. First controlled client experiment (Task 8, PROPOSED — not executed here)
|
||||
Goal: prove a patched client sends zero-pack Store entry to Browse Packs with **no**
|
||||
active placeholder.
|
||||
- Build `openfut-hook` with strategy-A `store_hook`, gated behind `openfut.cfg`
|
||||
`store_mypacks_fix=1` (opt-in; default off preserves today's behavior).
|
||||
- Test profile: `unopenedPackIds == []`.
|
||||
- Sequence (each variable changed alone; operator drives FIFA; read-only capture):
|
||||
1. Deploy patched `version.dll`; confirm `openfut_hook.log` shows the store hook
|
||||
installed + build gate PASSED.
|
||||
2. **Backend test mode (LATER, separately authorized — NOT in this task):** switch the
|
||||
backend to *empty-no-sentinel* (the Exp-B config that crashed the UNPATCHED client)
|
||||
so the patched client must handle a genuinely-absent `mypacks` group.
|
||||
3. Operator opens Store. **Predicted (patched + zero packs + no sentinel):** Store
|
||||
opens, defaults to Browse Packs, no `mypacks` resolve, **no crash, no dialog, no
|
||||
fake tile**.
|
||||
4. Set `unopenedPackIds=[70]`; reopen. **Predicted:** My Packs works normally
|
||||
(hook body skipped because count>0).
|
||||
5. Revert backend to the active sentinel.
|
||||
- **Backend change eventually required for this experiment: YES** — a controlled
|
||||
empty-no-sentinel test mode to force the absent group. It is NOT performed in this
|
||||
phase and MUST be separately authorized (same experiment discipline: patch the
|
||||
container copy, capture, revert, restart; never synthesize a client request).
|
||||
- **Client rollback:** flip `store_mypacks_fix=0` (hook not installed) or restore the
|
||||
original `version.dll`; the game reverts to depending on the backend sentinel. No FIFA
|
||||
binaries/movies/config are modified on disk — the hook is in-memory only, so rollback
|
||||
is a file/flag swap.
|
||||
|
||||
## 14. Interaction with the backend 65534 fallback (ESTABLISHED + PROPOSED)
|
||||
- **Keep the backend sentinel deployed** until strategy A is implemented AND verified.
|
||||
It remains the required behavior for unpatched retail clients and for any client whose
|
||||
build gate fails.
|
||||
- Once strategy A is verified, the server MAY, **for patched clients only**, omit the
|
||||
`mypacks` group when empty (the safe representation the client will then handle) —
|
||||
but only behind explicit detection/opt-in; do NOT drop the sentinel globally, since
|
||||
unpatched clients still crash without it.
|
||||
|
||||
## 15. ESTABLISHED / PROPOSED / UNKNOWN summary
|
||||
- **ESTABLISHED:** binary hashes/build; the full native category path and addresses
|
||||
(`FUN_18007d880/18007dab0/18007e7f0/1800147f0/180014420/180014580/180014610`); the
|
||||
instruction-level crash (`0x14882`, `[NULL+0x48]`); tab→ordinal map; that My Packs is
|
||||
not special-cased and funnels through `FUN_1800147f0`; the unopened-count singleton
|
||||
and its `vtbl[0x4d8]/[0x4e0]` accessors, reachable from store code; the
|
||||
`openfut-hook`/`version.dll` vehicle and its hook/patch primitives.
|
||||
- **PROPOSED (not implemented):** the strategy-A entry hook and its redirect logic; the
|
||||
build-guard scheme; the opt-in config flag; the first experiment and its backend
|
||||
test-mode requirement; the per-patched-client server relaxation.
|
||||
- **UNKNOWN:** exactly why the packed Scaleform movie selects My Packs on store open
|
||||
(Denuvo-packed, unread) — not needed for strategy A, which intercepts the native
|
||||
result; the precise rendered appearance of `category==0` list-all in this empty
|
||||
configuration (to be observed in the experiment); whether any non-store path also
|
||||
drives `screen+0x290` to a My-Packs ordinal (none found; `FUN_18007e7f0` case `0x7551`
|
||||
and the ctor are the only writers).
|
||||
|
||||
---
|
||||
|
||||
# PART III — Final no-sentinel resolver experiment (2026-08-13) — RESULT F3 (CRASH), CONFOUNDED
|
||||
|
||||
Vehicle change: the resolver guard was implemented as an **`autopatch.py` memory patch**
|
||||
(the live FIFA-17 client-patch mechanism), NOT the `version.dll` proxy — Proton loads its
|
||||
builtin `version.dll`, so the earlier `store_hook`/`version.dll` prototype was inert and
|
||||
has been rolled back. Guard: at CardsDLL `0x180014858`, `JNZ 0x14869` (`75 0f`) →
|
||||
`JG 0x14869` (`7f 0f`), orig-verified; routes category `< 0` (and `== 0`) to the safe
|
||||
list-all/Browse path (`FUN_180014610`), category `> 0` to the existing resolver.
|
||||
`TEST EDI,EDI` at `0x180014856` is the flag source (OF cleared ⇒ `JG` = signed `> 0`).
|
||||
|
||||
## Setup (verified)
|
||||
- CLIENT: guard active/enforced — live bytes `85 ff 7f 0f` at `0x180014856` (FIFA pid 547843,
|
||||
autopatch pid 547621; log `ENFORCED guarded store patch @ … (JNZ->JG)`, orig `75 0f` matched).
|
||||
- BACKEND: sentinel 65534 suppressed by a one-line `if not owned_ids:` → `if False:` in the
|
||||
container copy only (committed source `f42279f` untouched; backup `/tmp/utas_server.EXP_ORIG.py`).
|
||||
Genuine `GET /store/purchasegroup` (02:44:39Z) → ids `[1,5,6,7]`, **no 65534, no mypacks group**,
|
||||
normal packs unchanged (evidence: `docs/evidence/store_purchasegroup_capture_client_guard_no_sentinel_2026-08-13.json`).
|
||||
- PROFILE: `unopenedPackIds=[]`, coins 29,876,776, sha `39bb3e83…` — unchanged throughout.
|
||||
|
||||
## Result — F3 (CRASH)
|
||||
Minidump `CrashDump_2026.08.12_20.44.40.302.dmp` (preserved `/tmp/expF_crash.dmp`, sha `4dcb0cb7…`):
|
||||
`0xC0000005` READ of VA `0x48` at `ExceptionAddress 0x6ffffc224882` → **RE `0x180014882`** —
|
||||
the **identical** resolver crash instruction as Experiment B (`FUN_1800147f0`,
|
||||
`MOV R8,[RDX+0x8]` with the group ptr NULL).
|
||||
|
||||
**Mechanism (decisive):** `0x14882` lives in the *resolve* branch, which the guard's `JG`
|
||||
reaches **only when category `> 0`**. Since the guard was verified in place, the client
|
||||
presented a **positive** My-Packs ordinal that no longer resolves (no mypacks group) →
|
||||
`FUN_180014420` returned NULL → crash. The guard's design assumption — *absent mypacks ⇒
|
||||
category `-1`* — did NOT hold on this path.
|
||||
|
||||
## Confound (uncontrolled variable)
|
||||
FIFA was **not relaunched** after the backend flipped to no-sentinel; the client carried
|
||||
**stale store/tab state** from the sentinel-present safe stage, where the mypacks group
|
||||
existed at a *positive* ordinal `N` (`MYPACK_CATEGORY_ID = N`). Reopening the Store reused
|
||||
that stale positive ordinal rather than the `-1` a **fresh** launch publishes
|
||||
(`FUN_18007df60 → FUN_180014580(store,0) = -1` when absent). So the intended clean A/B (client
|
||||
only ever sees the no-sentinel response) was not achieved — the category that reached the
|
||||
resolver was a stale `>0`, exactly the case the negative-only guard does not divert.
|
||||
|
||||
## Conclusion / strategy status
|
||||
- **The guard as-written does NOT handle a positive, now-invalid My-Packs ordinal** — proven
|
||||
by this crash. Diverting only `category < 0` is insufficient when the client presents a
|
||||
stale/positive ordinal for an absent group.
|
||||
- **Not falsified for the fresh-client case.** Whether a fresh no-sentinel launch presents
|
||||
`-1` (guard diverts → Browse, no crash) or still a positive ordinal is **UNKNOWN** and needs
|
||||
a **clean re-test**: launch FIFA fresh with the backend already in no-sentinel mode so the
|
||||
client never sees a mypacks group. That is the proper equivalent of Experiment B.
|
||||
- **Candidate stronger guard** (design only, not implemented): divert to list-all when the
|
||||
resolved group is NULL for *any* category (guard `FUN_180014420`'s NULL return at the
|
||||
`0x14870`/`0x14882` site), not merely when `category < 0`. This covers the positive-invalid
|
||||
ordinal too, at the cost of being a generic miss-fallback (the higher-risk Rank-2 behavior).
|
||||
Do NOT implement without authorization and a clean re-test first.
|
||||
|
||||
**Strategy A / resolver guard status: NOT PROVEN.** Crash-guard installs and is build-validated
|
||||
and dormant-safe with the sentinel present, but the first no-sentinel test CRASHED at the
|
||||
resolver via a positive stale ordinal (confounded by no relaunch). Backend P2 active-sentinel
|
||||
was restored immediately (mandatory rollback; source `f416e71e…`, sentinel `state=active`),
|
||||
and remains the production safety net. Guard left in `autopatch.py` (dormant) pending the
|
||||
clean re-test decision; `autopatch.py.pre-storeguard.bak` available to remove it.
|
||||
|
||||
---
|
||||
|
||||
# PART IV — Fresh-process no-sentinel retest (2026-08-13) — RESULT R1 (SUCCESS)
|
||||
|
||||
Corrects PART III's confound. This time the mandatory ordering was enforced: the backend
|
||||
entered no-sentinel mode **while FIFA was closed**, then FIFA launched **fresh** (new pid,
|
||||
new autopatch) so the process never saw a sentinel-present Store response.
|
||||
|
||||
## Setup (verified, clean A/B)
|
||||
- BACKEND set no-sentinel at 02:55:09Z with FIFA down; genuine `GET /store/purchasegroup`
|
||||
(02:58:45Z) served to the fresh client = ids `[1,5,6,7]`, **no 65534, no mypacks group**,
|
||||
packs 1/5/6/7 present. This body is **byte-identical** to the PART III (F3) no-sentinel
|
||||
capture — the ONLY changed variable vs F3 is the client process lifetime.
|
||||
Evidence: `docs/evidence/store_purchasegroup_capture_freshretest_no_sentinel_2026-08-13.json`.
|
||||
- CLIENT: NEW FIFA pid 553220, NEW autopatch pid 552999; guard ENFORCED (orig `75 0f`
|
||||
matched → `85 ff 7f 0f` = `TEST EDI,EDI; JG`). Process never saw a sentinel response
|
||||
(0 purchasegroup responses containing 65534 after the no-sentinel restart).
|
||||
- PROFILE unchanged throughout (`39bb3e83…`, `[]`, coins 29,876,776).
|
||||
|
||||
## Result — R1 (operator-observed)
|
||||
- **No crash** (FIFA 553220 alive after the test; no new minidump), **no dialog**, **Store
|
||||
stays open**, opens on **Browse Packs**, Bronze/Gold/Special packs visible and navigable.
|
||||
- Cosmetic-only imperfections (pre-existing, NOT caused by the guard): the six-tab bar is
|
||||
unbound (no tabs), packs render without cover art, and tiles show "0 items". These match
|
||||
the known store tab-bind / list-all rendering quirks (`plan-2026-08-05-store-subsystem.md`
|
||||
§2.1) and are independent of the resolver guard.
|
||||
|
||||
## Causal conclusion (decisive A/B)
|
||||
```
|
||||
server response (no sentinel, no mypacks group) == byte-identical across F3 and R1
|
||||
client original JNZ + this response -> CRASH 0x180014882 (Experiment B)
|
||||
client JG (stale positive ordinal) -> CRASH 0x180014882 (PART III F3, contaminated)
|
||||
client JG (FRESH, category = -1) -> NO CRASH, Browse Packs (PART IV R1) ✅
|
||||
```
|
||||
A **fresh** client publishes `MYPACK_CATEGORY_ID = FUN_180014580(store,0) = -1` for the absent
|
||||
group; the movie echoes `-1`; `TEST EDI,EDI; JG` does **not** take the resolve branch, so the
|
||||
client runs the list-all/Browse path (`FUN_180014610`) — no `FUN_180014420(NULL)` deref, no
|
||||
crash. **PART III's F3 is confirmed as stale-positive-ordinal contamination** (FIFA not
|
||||
relaunched across the sentinel→no-sentinel flip), not a guard failure.
|
||||
|
||||
## Strategy status
|
||||
**Strategy A / resolver guard: PROVEN ON THE TESTED FIFA 17 BUILD** (CardsDLL
|
||||
`4706a881…`) for the clean process-lifetime case — it safely routes the absent My-Packs
|
||||
category to Browse Packs with no crash and no dialog, needing **no** backend sentinel. Scope
|
||||
caveats: (1) tested build only; (2) the negative-only guard does NOT cover a stale/positive
|
||||
invalid ordinal (PART III) — only arises if the client's Store state predates a sentinel→
|
||||
no-sentinel change within one process, which does not happen on a normal launch; a NULL-return
|
||||
guard at `FUN_180014420` would additionally cover that, deferred/not implemented; (3) UX still
|
||||
has the pre-existing no-tabs/no-art/"0 items" cosmetics.
|
||||
|
||||
Backend P2 active-sentinel was restored immediately after capture (mandatory rollback; source
|
||||
`f416e71e…`, sentinel `state=active`) and **remains production default**. The clean UX is only
|
||||
safe to serve when the server knows the client is patched — see PART II §12 rollout options
|
||||
(recommend B: suppress the sentinel only when client patch-capability is known; keep the
|
||||
sentinel universal by default). Guard retained in `autopatch.py` (dormant with the sentinel).
|
||||
|
||||
## INVARIANT — empty-My-Packs capability MUST be session-stable
|
||||
|
||||
F3 vs R1 establish a hard operational invariant for any deployment (sentinel or client
|
||||
guard): **the server MUST NOT switch a running FIFA client between sentinel-present and
|
||||
sentinel-absent for the My Packs group within a single FIFA process lifetime.**
|
||||
|
||||
Rationale: the client resolves and caches the My-Packs group **ordinal** (positive when a
|
||||
group — real or sentinel — is present; `-1` when absent) from the `purchasegroup` response
|
||||
seen at Store-subsystem init. The resolver guard only reclassifies the ordinal *sign*
|
||||
(`≤0` → Browse). If a client that already cached a **positive** ordinal later receives a
|
||||
no-sentinel topology, the stale positive ordinal still takes the resolve branch and
|
||||
`FUN_180014420` returns NULL → crash at `0x180014882` (exactly F3). A **fresh** process that
|
||||
only ever sees the no-sentinel topology caches `-1` and is routed to Browse safely (R1).
|
||||
|
||||
Practical rules:
|
||||
- Choose the My-Packs representation (sentinel-present vs sentinel-absent) **before** a client
|
||||
starts its session, and hold it for that session.
|
||||
- The future patch-capability handshake (PART II §12) MUST therefore be decided at
|
||||
login/session start, not toggled mid-session.
|
||||
- A NULL-return guard at `FUN_180014420` (deferred) is the only thing that would make a
|
||||
mid-session flip crash-safe; until then, session stability is mandatory.
|
||||
@@ -1,358 +0,0 @@
|
||||
# FIFA 17 — verified patched-client capability negotiation
|
||||
|
||||
Goal: let the FIFA 17 backend suppress the synthetic My-Packs sentinel (id 65534)
|
||||
**only when the current FIFA process has positively verified that the CardsDLL
|
||||
resolver guard is active** (JNZ→JG at RVA `0x14858`). Unpatched / unsupported /
|
||||
unknown / failed-patch clients keep receiving the existing P2 active sentinel.
|
||||
|
||||
Core principle: **the capability is not "this launcher supports the patch"; it is
|
||||
"the resolver guard was verified in *this particular FIFA process*."**
|
||||
|
||||
This document is the design + the cross-component contract. It is deliberately
|
||||
additive: the P2 active-sentinel path (`docs/evidence/FIFA17_EMPTY_MYPACKS_CLIENT_CONTRACT.md`)
|
||||
remains the default and the universal fallback.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture inventory (as-built, verified by reading the code)
|
||||
|
||||
Data flow today (launch of one FIFA process):
|
||||
|
||||
```
|
||||
LauncherApp::launch_game (openfut-launcher/src/app.rs:450)
|
||||
-> account_sync::sync POST /openfut/account/sync (:8099) [REQUIRED; launch is gated on it]
|
||||
-> ensure_local_services() spawn LSX, then autopatch.py --launcher-pid <launcher_pid>
|
||||
-> game_launch::launch umu-run FIFA17.exe (grandchild; launcher never learns FIFA PID)
|
||||
FIFA process
|
||||
-> autopatch.py self-discovers FIFA by comm=='FIFA17.exe'; patches /proc/<pid>/mem each tick
|
||||
-> FIFA -> backend POST /ut/auth (login) ; GET /store/purchasegroup ; ... (:8099)
|
||||
```
|
||||
|
||||
Facts that shape the design:
|
||||
|
||||
- **Launcher ↔ autopatch IPC = one-way stdout only.** `local_services::spawn`
|
||||
(openfut-launcher/src/local_services.rs:279-296) pipes autopatch stdout/stderr
|
||||
into the launcher `LogBuffer` line-by-line as `[autopatch] <line>`. There is no
|
||||
socket / named pipe / status-file readback. `--launcher-pid` is the *launcher's*
|
||||
own pid (local_services.rs:66), used for liveness, not to identify FIFA.
|
||||
- **Launcher ↔ backend = exactly one control call:** `account_sync::sync`
|
||||
(openfut-launcher/src/account_sync.rs:43) — a tiny stdlib-HTTP `POST
|
||||
/openfut/account/sync` on `openfut_account_sync_port` (default 8099), sent once
|
||||
per launch, *before* FIFA starts, and **launch is blocked unless it succeeds**
|
||||
(utas_server.py:1204). This is the reliable per-FIFA-process session boundary.
|
||||
- **Backend is single-account, stateless-per-request, threaded.** `SID` is a fixed
|
||||
module constant shared by all clients (utas_server.py:32); account identity is one
|
||||
global `ACCOUNT` singleton. There is **no per-session identity** in requests. The
|
||||
only per-connection discriminator available at every handler is
|
||||
`self.client_address[0]` (peer IP), currently unused. Server is
|
||||
`ThreadingHTTPServer` (utas_server.py:3784); module is import-safe (server under
|
||||
`if __name__ == "__main__"`).
|
||||
- **No bridge/proxy in the FIFA-17 path.** FIFA reaches the Python backend's
|
||||
published `:8099` directly (client-side DNAT/hosts redirect); the openfut-bridge is
|
||||
legacy FIFA-23. Docker's iptables DNAT preserves the source IP for external LAN
|
||||
clients. The launcher and FIFA run on the **same** client machine, so the backend
|
||||
observes them under the **same** peer IP regardless of NAT.
|
||||
|
||||
## 2. Capability transport — options and choice
|
||||
|
||||
Ranked against the as-built architecture:
|
||||
|
||||
**autopatch → launcher (chosen: structured stdout line).**
|
||||
1. **Structured stdout line (CHOSEN).** Reuses the existing one-way pipe the
|
||||
launcher already reads. It is *live* (only the current autopatch child's stdout),
|
||||
inherently child-bound, and carries **zero stale-file risk** — a previous
|
||||
launch's capability cannot leak because nothing is persisted. Smallest possible
|
||||
change. Format is a machine-readable token (§4).
|
||||
2. Status file in `$XDG_RUNTIME_DIR` keyed by launcher-pid+FIFA-pid+version+timestamp
|
||||
— works but needs explicit staleness handling and cleanup; more moving parts.
|
||||
3. Unix-domain socket — most capable but overkill; there is no bidirectional need.
|
||||
|
||||
**launcher → backend (chosen: sibling HTTP endpoint on the account-sync port).**
|
||||
- A. **Existing session-init channel (CHOSEN).** Add `POST /openfut/fifa17/capability`
|
||||
next to the existing `/openfut/account/sync` (same port 8099, same tiny stdlib-HTTP
|
||||
client). It cannot ride *inside* account_sync because the capability is only known
|
||||
*after* autopatch verifies (which happens after account_sync + FIFA start), so it is
|
||||
a separate, later call — but on the same proven transport.
|
||||
- B. Blaze/login metadata — rejected: no OpenFUT-owned field is available without
|
||||
risking a field FIFA depends on, and Blaze runs in a separate responder.
|
||||
- C. New local IPC + backend side-channel — unnecessary; A already exists.
|
||||
- D. Server-wide "assume patched" config — dev/testing fallback only; cannot
|
||||
distinguish patched vs unpatched clients, so never the production mechanism.
|
||||
|
||||
## 3. The capability (name + version + VERIFIED semantics)
|
||||
|
||||
- Name: **`fifa17.empty_mypacks_resolver`**, integer version, current **`1`**.
|
||||
- **VERIFIED (v1) means, for THIS FIFA process:** the CardsDLL tested build was
|
||||
recognised AND the live bytes at RVA `0x14858` are `7f 0f` (`JG`) **after
|
||||
autopatch enforcement** — i.e. `guarded_action` returned `"patch"` (was `75 0f`,
|
||||
written, re-read as `7f 0f`) **or** `"noop"` (already `7f 0f`).
|
||||
- It explicitly does **NOT** mean any of: "autopatch.py contains the guard code",
|
||||
"the launcher build is new enough", or "a config flag is set". The signal
|
||||
represents **observed runtime enforcement on the specific process**, nothing less.
|
||||
|
||||
## 4. autopatch verification state + emitted line
|
||||
|
||||
Per-FIFA-pid guard status (fail-closed; never loosens the existing byte guard):
|
||||
|
||||
| state | meaning |
|
||||
|---|---|
|
||||
| `NOT_ATTEMPTED` | CardsDLL not yet mapped / guard not evaluated for this pid |
|
||||
| `VERIFIED` | live bytes == `7f 0f` after enforcement (from `patch` or `noop`) |
|
||||
| `UNSUPPORTED_BUILD` | live bytes are neither the known original nor patched (`guarded_action` → `skip`) |
|
||||
| `WRITE_FAILED` | `/proc/<pid>/mem` write raised |
|
||||
| `VERIFY_FAILED` | post-write re-read != `7f 0f` |
|
||||
|
||||
Only `VERIFIED` advertises capability. On transition to `VERIFIED`, autopatch emits
|
||||
**once per FIFA pid** on stdout:
|
||||
|
||||
```
|
||||
[store-guard] verified capability fifa17.empty_mypacks_resolver=1 fifa_pid=<pid>
|
||||
```
|
||||
|
||||
Any non-verified terminal state emits an explicit, non-advertising status line, e.g.:
|
||||
|
||||
```
|
||||
[store-guard] guard status=UNSUPPORTED_BUILD fifa_pid=<pid> (no capability advertised)
|
||||
```
|
||||
|
||||
## 5. Launcher per-process capability state
|
||||
|
||||
```rust
|
||||
pub struct Fifa17ClientCapabilities { pub empty_mypacks_resolver: Option<u32> }
|
||||
```
|
||||
|
||||
- Starts **UNKNOWN** (`None`) at each launch.
|
||||
- Becomes `Some(1)` when the launcher parses a valid capability line from the
|
||||
**current** autopatch child's stdout (`parse_capability_line`).
|
||||
- **Discarded** when autopatch stops / FIFA exits / launcher exits / next launch. It
|
||||
is never persisted and never reused for a later FIFA process — staleness is
|
||||
structurally impossible.
|
||||
|
||||
On first `Some(v)`, the launcher registers the capability with the backend (§6) once.
|
||||
|
||||
## 6. Launcher → backend registration + binding
|
||||
|
||||
`POST /openfut/fifa17/capability` (port = `openfut_account_sync_port`, 8099), body:
|
||||
|
||||
```json
|
||||
{"capability":"empty_mypacks_resolver","version":1,"personaId":<id>,"fifaPid":<pid>}
|
||||
```
|
||||
|
||||
- **Binding key = source IP** (`self.client_address[0]`). The registration arrives
|
||||
from the client machine's IP; FIFA's `/store/purchasegroup` requests arrive from
|
||||
the **same** IP (same machine). `personaId`/`fifaPid` are for logging only (the
|
||||
backend is single-account, so persona cannot discriminate clients).
|
||||
- Concurrency: distinct client machines → distinct peer IPs → independent decisions
|
||||
(no global state). Two FIFA processes on **one** machine share an IP — an accepted
|
||||
limitation (the backend is single-account anyway); documented in §Trust.
|
||||
|
||||
## 7. Backend session-stable decision
|
||||
|
||||
Per-IP record (guarded by a lock; threaded server):
|
||||
|
||||
```
|
||||
_FIFA17_STORE[ip] = {"resolver": Option[int], "mode": Option[str]} # mode: None|"sentinel"|"clean-v1"
|
||||
```
|
||||
|
||||
- **Reset (session boundary):** `/openfut/account/sync` from `ip` sets
|
||||
`{resolver: None, mode: None}`. This is the launcher's required per-launch call, so
|
||||
every new FIFA process starts from a clean, unfrozen record — no cross-process leak.
|
||||
- **Register:** `/openfut/fifa17/capability` from `ip` sets `resolver = version`. If
|
||||
`mode` is already frozen, it is logged as late and **ignored for this session**.
|
||||
- **Freeze point = first `/store/purchasegroup`** from `ip` (§9): if `mode is None`,
|
||||
set `mode = "clean-v1"` iff `resolver == 1` else `"sentinel"`, and log once.
|
||||
Thereafter `mode` is immutable for the session.
|
||||
- **Default / fail-closed:** an IP with no record (no account-sync, no capability),
|
||||
an unknown resolver version, a late capability, or a disappeared capability all
|
||||
resolve to (or remain) `"sentinel"`.
|
||||
|
||||
## 8. Freeze point rationale
|
||||
|
||||
Freeze at **first `/store/purchasegroup`**, not at login/account-sync. account-sync
|
||||
fires *before* FIFA starts and *before* autopatch can verify, so freezing there would
|
||||
always be `sentinel`. First Store request is the earliest moment at which a genuine
|
||||
capability can already be registered (autopatch verifies at process start; the user
|
||||
opens the Store later), while still being a single, well-defined topology commit for
|
||||
the session. Once Store topology is served, it must not change (the F3 experiment
|
||||
proved a mid-session flip can leave a stale positive ordinal that crashes even the
|
||||
sign-only guard — see `FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md` PART III/IV and the
|
||||
SESSION-STABLE invariant).
|
||||
|
||||
## 9. Store behaviour (additive switch)
|
||||
|
||||
At `store_catalog`, only the zero-owned-packs branch changes:
|
||||
|
||||
```
|
||||
if not owned_ids:
|
||||
if fifa17_empty_mypacks_mode(client_ip) == "clean-v1":
|
||||
pass # patched client: emit NO mypacks group; guard routes -1 to Browse
|
||||
else:
|
||||
<append active 65534 sentinel exactly as today> # P2 fallback (unchanged)
|
||||
```
|
||||
|
||||
Untouched: real owned-pack rendering, `PACK_CATALOG`, pack 70, normal packs 1/5/6/7,
|
||||
profile state, all store env flags. Default remains sentinel. This lives in the FIFA-17
|
||||
Python backend only — **never** in game-independent OpenFUT Core.
|
||||
|
||||
| client | zero packs | real unopened pack |
|
||||
|---|---|---|
|
||||
| verified v1 | **no sentinel** (clean) | genuine My Packs, no sentinel |
|
||||
| no / unknown capability | **active 65534 sentinel** | genuine My Packs, no sentinel |
|
||||
|
||||
## 10. Trust model (Task 14)
|
||||
|
||||
This is **not** anti-cheat / attestation. OpenFUT assumes the user controls the
|
||||
launcher/client machine and the server is a private preservation environment. The
|
||||
verification exists to prevent *accidents*: a stale capability, an unsupported
|
||||
CardsDLL build, a failed autopatch, the wrong process, or an unpatched client
|
||||
receiving no sentinel and crashing. No signatures / PKI / remote attestation.
|
||||
|
||||
Isolation across *distinct client machines* relies on the backend observing distinct
|
||||
peer IPs (source-IP-preserving publish; Docker's default for external LAN via iptables
|
||||
DNAT). Two FIFA processes on one machine cannot be distinguished by IP — accepted,
|
||||
since the backend is single-account. The single-client production case is unaffected
|
||||
by NAT because launcher and FIFA share one IP.
|
||||
|
||||
## 11. Fail-closed matrix (Task 15) — every failure ⇒ sentinel
|
||||
|
||||
autopatch missing / not run · guard `UNSUPPORTED_BUILD` / `WRITE_FAILED` /
|
||||
`VERIFY_FAILED` · launcher cannot parse the line · registration POST fails ·
|
||||
account-sync never called · unknown capability version · capability arrives after
|
||||
freeze · capability disappears after a sentinel freeze — **all resolve to the active
|
||||
65534 sentinel.** Asserted by tests (matrix A–J) and this document.
|
||||
|
||||
## 12. P2 retained (Task 16)
|
||||
|
||||
The active-sentinel implementation is **not** removed. It is the else-branch of the
|
||||
switch and the universal default for unpatched clients, unsupported builds, failed
|
||||
patches, unknown launchers, and late capabilities. The clean path is purely additive.
|
||||
|
||||
---
|
||||
|
||||
## 13. Session binding (hardening — supersedes the per-IP prototype)
|
||||
|
||||
**History.** The first implementation keyed the backend capability/store-mode by
|
||||
**source IP alone** (§7 as originally written). That was rejected before deployment:
|
||||
two FIFA processes that share a source IP — concurrent, or a relaunch — would share
|
||||
the key, so an *unverified* process could inherit a *verified* one's `clean-v1`
|
||||
topology and crash on the empty-My-Packs resolver. Source IP is now **auxiliary only**
|
||||
(logging, a fail-closed sid/ip sanity check, and the pending hand-off key). This
|
||||
history is retained deliberately; do not treat per-IP as the design.
|
||||
|
||||
**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 `OPENFUT-SID-…0001`); the
|
||||
client echoes it on every later call, and it is **live-confirmed present on real
|
||||
`/store/purchasegroup` requests**. The SID uniquely identifies one FIFA process/login:
|
||||
a relaunch re-auths → new SID; two concurrent logins → two SIDs. The legacy constant
|
||||
is still accepted by the retired security-question gate only, and is **never** used to
|
||||
grant `clean-v1`. A store request whose SID was opened on a different source IP is
|
||||
fail-closed to sentinel (sid/ip sanity check).
|
||||
|
||||
**Why not persona alone:** the backend is single-account, so `personaId` cannot
|
||||
distinguish two sessions, and a relaunch keeps the same persona — persona alone would
|
||||
leak a prior session's mode. Persona is used only (with IP) to key the pending hand-off.
|
||||
|
||||
### State machine (per session, keyed by SID)
|
||||
```
|
||||
Capability : Unknown | ResolverV1
|
||||
StoreMode : Unfrozen | Sentinel | CleanV1
|
||||
|
||||
/ut/auth (new SID) : Capability=Unknown, StoreMode=Unfrozen, record {ip,persona}
|
||||
+ consume any pending (ip,persona) -> Capability=ResolverV1
|
||||
capability registered : bind to the one live Unfrozen/Unbound session for (ip,persona)
|
||||
-> Capability=ResolverV1 ; else stage single-use pending ;
|
||||
else (a session exists but is frozen/ambiguous) -> ignored-late
|
||||
first /store/purchasegroup : Unfrozen + ResolverV1 -> freeze CleanV1
|
||||
Unfrozen + otherwise -> freeze Sentinel (consume pending first)
|
||||
late capability : StoreMode already frozen -> unchanged (ignored-late, not staged)
|
||||
capability lost/cleared : after a CleanV1 freeze -> stays CleanV1 (mode is cached)
|
||||
session idle > TTL / reaped: session discarded (a later store with that SID -> Sentinel)
|
||||
```
|
||||
|
||||
### Registration order + pending hand-off
|
||||
The verified capability is known only after the FIFA process exists, CardsDLL is
|
||||
loaded, and autopatch confirms the JG bytes — which may land before or after
|
||||
`/ut/auth`, but reliably before the user opens the Store. The launcher cannot know
|
||||
the SID, so its registration is matched to a session by (source_ip, persona) as a
|
||||
**single-use, short-TTL pending** (`FIFA17_PENDING_TTL = 120s`) that is consumed by
|
||||
exactly one session, at whichever of these happens first for that session: its
|
||||
`/ut/auth` (pending predates login), the registration itself (session already live —
|
||||
bound directly), or its first store request (lazy). If the Store is reached before a
|
||||
capability binds, the session freezes **Sentinel** (fail-closed); a later capability
|
||||
does not change it.
|
||||
|
||||
### Session cleanup (Task 10)
|
||||
- **creation:** at `/ut/auth`.
|
||||
- **last activity:** bumped on every `/store/purchasegroup` for the session.
|
||||
- **freeze:** first `/store/purchasegroup`.
|
||||
- **expiry:** lazy sweep on every session op removes sessions idle for
|
||||
`FIFA17_SESSION_TTL = 3600s` and pendings older than `FIFA17_PENDING_TTL`. Explicit
|
||||
Blaze/UTAS teardown is not reliably observable at this handler, so a conservative
|
||||
activity-based TTL is used instead. Reaping only removes *expired* entries and never
|
||||
affects another live session from the same IP/persona (keyed by distinct SIDs).
|
||||
|
||||
### Residual limitation (documented, fail-closed)
|
||||
FIFA carries no launcher-controllable per-process token, so two **simultaneous** logins
|
||||
from the **same (ip, persona)** cannot be disambiguated at the instant a capability is
|
||||
registered while *both* are Unfrozen/Unbound. That ambiguous case resolves to
|
||||
`ignored-late` → **both freeze Sentinel** (safe: an unverified process is never granted
|
||||
clean). The normal one-launcher-per-FIFA and sequential-relaunch flows bind correctly
|
||||
(proven by matrix K/L/M). This is a UX conservativeness, never a safety hole.
|
||||
|
||||
---
|
||||
|
||||
## 14. Deployment candidate & controlled A/B (overnight reconciliation 2026-08-13)
|
||||
|
||||
**Launcher lineage reconciliation.** The two divergent launcher histories (merge
|
||||
base `87241ac`) were reconciled by a real merge — **not** a rebase/squash/rewrite —
|
||||
in a clean worktree:
|
||||
- `feat/launcher-arming` `13339c1` (client arming + FIFA-17 capability reporting)
|
||||
- `feat/sbc-hook-tracing` `958ff24` (openfut-hook SBC request tracing / RE probes)
|
||||
|
||||
Merged commit **`ca7ce26`** on branch `integration/fifa17-launcher-capability-sbc`
|
||||
retains **both** ancestors (`git merge-base --is-ancestor` true for both `958ff24`
|
||||
and `13339c1`). The only conflict was `src/process.rs` (launcher-arming deleted it +
|
||||
dropped `mod process`; SBC only incidentally tidied it) — resolved **keep-deleted**
|
||||
(orphan module; the SBC feature lives entirely in `openfut-hook/*`). The two features
|
||||
are in disjoint crates/processes (launcher-crate Rust host vs `openfut-hook` Windows
|
||||
DLL) and share no stdout readers, child handles, or lifecycle — no integration code
|
||||
was needed.
|
||||
|
||||
**Gitlink status — DEFERRED (morning blocker).** The superproject gitlink still
|
||||
records the pre-reconciliation `958ff24`. It was **not** bumped to `ca7ce26` because
|
||||
the live submodule checkout carries uncommitted `openfut-hook/*` WIP that overlaps the
|
||||
merged hook content; a non-destructive `git checkout ca7ce26` is refused ("local
|
||||
changes would be overwritten"), and no `-f`/`reset`/`clean` is permitted. The user
|
||||
must first reconcile that WIP against the merged `openfut-hook`, then the gitlink can
|
||||
bump. Preservation artifact: `/tmp/openfut-launcher-overnight-tracked.patch`
|
||||
(sha256 `8e65de2c…`).
|
||||
|
||||
**Validated deployment-candidate tuple** (reproducible from git except the deferred
|
||||
gitlink):
|
||||
```
|
||||
superproject HEAD a82407c (backend per-session + docs)
|
||||
backend guard b0d5e04 fix(fifa17): guard missing store category resolution
|
||||
client proof fc29c2e docs(fifa17): record no-sentinel client resolver proof
|
||||
autopatch report 1c396dd feat(fifa17): report verified client patch capability
|
||||
backend negotiate b25761e feat(fifa17): negotiate clean empty My Packs mode
|
||||
session binding 805d754 fix(fifa17): isolate patched-client capability per session
|
||||
launcher merged HEAD ca7ce26 merge: reconcile launcher capability and SBC tracing
|
||||
(ancestors 13339c1 capability + 958ff24 SBC)
|
||||
launcher gitlink (super) 958ff24 <-- to become ca7ce26 once WIP reconciled
|
||||
```
|
||||
Local build artifacts (NOT deployed): launcher `target/release/openfut-launcher`
|
||||
(sha256 `a390c61d…`); backend image `openfut-fut-backend:candidate-overnight`
|
||||
(`84d280be…`, ships `utas_server.py` `33e0ef3…`). Live `:dev` image and the running
|
||||
container were left untouched.
|
||||
|
||||
### Controlled A/B sequence (execute only in a later authorized deploy task)
|
||||
**A — patched client:** fresh FIFA process → autopatch verifies the JG guard →
|
||||
launcher parses the verified line and registers → `/ut/auth` mints a fresh `X-UT-SID`
|
||||
→ capability binds to that SID → first `/store/purchasegroup` freezes `clean-v1` →
|
||||
backend omits 65534 → Store opens on Browse Packs, no crash.
|
||||
**B — unpatched client, same machine/IP, NEW session:** new `X-UT-SID`, no verified
|
||||
capability → first store freezes `sentinel` → backend emits active 65534 → no crash.
|
||||
Proves same-IP isolation + fail-closed fallback.
|
||||
**C — failed patch (optional):** autopatch reports `UNSUPPORTED_BUILD`/`VERIFY_FAILED`
|
||||
→ launcher never registers → `sentinel`.
|
||||
Production remains the P2 active-sentinel universal default until this A/B passes.
|
||||
@@ -1,208 +0,0 @@
|
||||
# OpenFUT Status Review
|
||||
*Generated 2026-06-30 — read-only stocktake, no code changed.*
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
OpenFUT has a mature offline FUT economy backend (Core, 25 phases, fully functional in
|
||||
isolation) and a sophisticated hook DLL that loads into FIFA 23, redirects EA hostnames
|
||||
to loopback, and bypasses TLS certificate verification. The Blaze/ProtoSSL layer is
|
||||
structurally ready: framing code exists, a TLS listener runs, cert-verify is patched.
|
||||
However the project is currently blocked before any Blaze traffic is ever seen.
|
||||
The fundamental problem is that FIFA 23 submits `GoOnline` to EbisuSDK and then
|
||||
**waits for an asynchronous ONLINE_STATUS_EVENT push** from the EA-app LSX server —
|
||||
a push that current code never sends. Every approach tried so far (flipping poll
|
||||
return values, forcing the state flags, read-only probes) confirms the gate is
|
||||
event-driven, not poll-driven. The Blaze captures directory contains six empty files.
|
||||
No Fire2 frame from FIFA 23 has ever been decoded. Until the ONLINE_STATUS_EVENT push
|
||||
is synthesized and delivered correctly, Milestones 2–7 are all waiting on the same
|
||||
single wall.
|
||||
|
||||
---
|
||||
|
||||
## 1. Proven vs Assumed
|
||||
|
||||
| Claim | Status | Evidence |
|
||||
|---|---|---|
|
||||
| FIFA 23 uses DirtySDK / ProtoSSL | **Proven** | String scan hit `ProtoSSLSend`, `ProtoSSLRecv`, `gosredirector` in FIFA23.exe memory (Task 1) |
|
||||
| `version.dll` loads and runs hook code | **Proven** | `hook.log` written at DLL_PROCESS_ATTACH |
|
||||
| `getaddrinfo` IAT hook redirects EA domains to loopback | **Proven** | Hook log records every EA `getaddrinfo` call; connect_hook log confirms port redirects |
|
||||
| ProtoSSL cert-verify prologue found and patched (FIFA23.exe) | **Proven** | ssl_patch.rs prologue confirmed at file offset 0xf0c850; hook log "ssl: main exe cert-verify patched" |
|
||||
| ProtoSSL cert-verify patched in EAWebKit.dll | **Proven** (if loaded) | Lazy patch fires on first EA getaddrinfo call; hook log message confirms |
|
||||
| Gate is upstream of DirtySDK — no DNS/connect fires on FUT entry | **Proven** | getaddrinfo, connect, WSASend/Recv hooks all show zero external traffic during "connecting to EA Servers" |
|
||||
| `GoOnline` is called by the game | **Proven** | Read-only detour on `anadius64.dll+0x2BB90` confirmed hit |
|
||||
| anadius returns GoOnline success | **Proven** | Handler observed returning successfully; game still retries every ~7 s |
|
||||
| Gate is downstream of GoOnline | **Proven** | GoOnline called + returns success; no Blaze connect follows |
|
||||
| Connection-state function: `GetInternetConnectedState @ anadius64.dll+0x27790` | **Proven** | Located via anadius LSX command-registration table; two-flag branch decoded (`+0xCAB1A`, `+0xCAB1B`) |
|
||||
| Gate is event-driven (game waits for async push, not a poll return) | **Proven** | Forced both state flags AND GoOnline return to "1"; game kept retrying; worker-thread stack scan confirms handler runs on anadius IOCP thread, not FIFA's thread |
|
||||
| GoOnline runs on anadius worker thread, not FIFA's call thread | **Proven** | Stack scan from inside detour found zero FIFA23.exe frames, sp ~2.4 KB from thread stack top |
|
||||
| `protossl-scan` live toolkit is exhausted for finding GoOnline in FIFA23.exe | **Proven** | No `"GoOnline"` string in image; worker-thread call stack has no FIFA frames; jmpscan yields ~3875 hits (overwhelmingly data false positives) |
|
||||
| FIFA 23 redirector config references `Authorization:` header (Nucleus token) | **Proven** | Found in FIFA23.exe .rdata pointer table @ `+0x83FC858` |
|
||||
| openfut-core REST API complete and tested | **Proven** | 25 phases, 15 migrations, passing integration tests |
|
||||
| Bridge LSX server starts and handles request-response | **Proven** (code) | `openfut-bridge/src/lsx.rs` + `main.rs` — server starts on 127.0.0.1:3216 |
|
||||
| Bridge LSX server ACTUALLY receives FIFA's LSX connections | **UNCONFIRMED** | anadius may intercept the same calls in-process before the TCP connection reaches the bridge |
|
||||
| Bridge LSX server `GetInternetConnectedState → connected="1"` unblocks the gate | **UNCONFIRMED (known to fail in-process)** | Flipping the value via anadius in-process failed; bridge path not yet confirmed working |
|
||||
| ONLINE_STATUS_EVENT push XML format | **UNKNOWN** | No capture; format not derived |
|
||||
| Fire2 framing is correct for FIFA 23 | **UNCONFIRMED** | Implemented based on post-2012 EA convention; all blaze captures are empty (0 bytes) |
|
||||
| Blaze component / command IDs for FIFA 23 | **UNKNOWN** | Zero captures; dispatch table entirely empty placeholders |
|
||||
| ProtoSSL recv-injection convention (non-blocking return values etc.) | **UNCONFIRMED** | Never reached M4; recv_hook module removed from active install path |
|
||||
| FUT REST endpoint paths in mapper.rs | **SPECULATIVE** | Based on community knowledge of older FIFA titles; the one actual capture in `captures/` is an early GET from before the Blaze strategy |
|
||||
| FLE Lua API exposes FUT DB tables in memory | **UNKNOWN** | `export_squad.lua` has never been run; FUT data may only exist server-side in online mode |
|
||||
|
||||
---
|
||||
|
||||
## 2. Milestone Status
|
||||
|
||||
| Milestone | Status | Blocker | Depends on unconfirmed assumption? |
|
||||
|---|---|---|---|
|
||||
| **M1** — Locate connection-state decision point | ✅ Done | — | No |
|
||||
| **M2** — Flip gate, force "connected" | ⛔ Blocked | Game waits for async ONLINE_STATUS_EVENT push; no current code sends it | Yes — unknown event XML format |
|
||||
| **M3** — First ProtoSSL plaintext on Blaze connection | 🔲 Not started | Depends on M2 | Yes — Fire2 framing unconfirmed |
|
||||
| **M4** — Answer redirector + decode first Fire2 frame | 🔲 Not started | Hard wall: Fire2 framing, recv-injection convention, component/command IDs all unconfirmed | Yes — all three unknown |
|
||||
| **M5** — Blaze preauth / login / postauth | 🔲 Not started | Depends on M4 | Yes — Blaze auth TDF body layout unknown |
|
||||
| **M6** — FUT entry + hub load | 🔲 Not started | Depends on M5; also requires FUT REST response shapes confirmed | Yes — endpoint paths speculative |
|
||||
| **M7** — Squad Battles (AI FUT) | 🔲 Not started | Depends on M6 | Yes |
|
||||
|
||||
**Note on roadmap.md wording:** Under M2–M4, roadmap.md uses `**Done (observable):**` bullets. These describe the *success criterion* for each milestone, not an achieved state. The authoritative status is in `connection-gate-findings.md` (M2 attempts failed; M3/M4 never started). The roadmap has not been updated to reflect M2 failure.
|
||||
|
||||
### M4 is the first hard wall in detail
|
||||
|
||||
Even assuming M2 is solved, M4 requires three unconfirmed things simultaneously:
|
||||
1. **Fire2 framing** — the 12-byte header layout is assumed; if FIFA 23 uses an older Fire variant or a custom delta, the codec will misparse every packet.
|
||||
2. **ProtoSSL recv-injection** — delivering responses to the game via recv hook requires knowing what return values and buffer conventions ProtoSSL expects; recv_hook.rs exists but is not installed.
|
||||
3. **Blaze component/command IDs** — the dispatch table is entirely empty; we cannot answer any request until IDs are known from captures.
|
||||
|
||||
All three are resolved by getting one real captured frame. M4 is primarily a capture problem, not a decoding problem — once bytes exist, the framing and IDs are immediately readable.
|
||||
|
||||
---
|
||||
|
||||
## 3. Blockers, Risks, Unknowns
|
||||
|
||||
### Blockers (stop progress now)
|
||||
|
||||
1. **ONLINE_STATUS_EVENT push not synthesized** *(M2 wall)*
|
||||
The game calls GoOnline, gets success, then waits indefinitely for a push event on the LSX socket that never arrives. This is the single gate blocking all Blaze work. Options: (a) trace the event format via Ghidra on FIFA23.exe (xref `ONLINE_STATUS_EVENT` string + the game's EbisuSDK listener), (b) RE anadius's LSX event-send path (find what it would push in an "online" scenario), (c) brute-force push candidate event XMLs and observe whether the game advances.
|
||||
|
||||
2. **Bridge LSX server delivery unconfirmed** *(architectural risk converted to blocker)*
|
||||
The hook passes port 3216 connections through, assuming the bridge LSX server on the Linux host receives them. If anadius's in-process hooks intercept the winsock calls before they reach the TCP stack, the bridge server is never reached. This must be confirmed by checking `openfut_hook.log` for a getaddrinfo on the LSX host, or by observing the bridge server's accept logs.
|
||||
|
||||
### Risks (could derail later)
|
||||
|
||||
3. **Fire2 framing wrong** *(M4 risk)*
|
||||
If FIFA 23 uses Fire (pre-2012) or a modified frame layout, the codec misparses. Mitigation: the server has a `Raw` fallback mode for capturing raw bytes when framing fails.
|
||||
|
||||
4. **Secondary auth-token gate** *(M5 risk)*
|
||||
`connection-gate-findings.md` noted the redirector request carries an `Authorization:` header. M1's final conclusion said `GetAuthCode` returns a fake token that appears accepted — but this was inferred, not confirmed by seeing the redirector request actually constructed with that token.
|
||||
|
||||
5. **EAAC not fully neutralized** *(persistent risk)*
|
||||
`FakeEAACLauncher` bypasses the anticheat launcher. The hook DLL is unsigned. If EAAC is ever active (e.g., after a game update re-enables it), all hooks fail silently. Marked as "not active in offline/cracked builds" — assumed, not confirmed on every launch.
|
||||
|
||||
6. **FUT REST response shapes wrong** *(M6 risk)*
|
||||
The 61 endpoint mappings in mapper.rs and the shaper stubs in shaper.rs are based on community guesses about older FIFA FUT APIs, not FIFA 23 captures. Response JSON shapes may differ enough to cause the client to fail silently or crash.
|
||||
|
||||
### Unknowns (open questions)
|
||||
|
||||
7. **ONLINE_STATUS_EVENT XML format** — exact tag names, field order, sender attribute, and any nonces/tokens required.
|
||||
8. **GoOnline event sequence** — whether ONLINE_STATUS_EVENT alone is sufficient or a sequence of events (e.g., PROFILE_EVENT, LOGIN_EVENT, COMMERCE_EVENT) is expected.
|
||||
9. **Whether FLE exposes FUT DB tables** — FUT card inventory and squad data likely live server-side in online mode; FLE may not surface them for in-process editing.
|
||||
10. **Blaze component/command IDs for FIFA 23** — entirely unknown; no captures.
|
||||
11. **openfut_hook.log current content** — we have the code but no log output in any document. Whether the current hook (with connect, ssl_patch, tls_bypass, WSAIoctl, origin_spy all installed) fires correctly and what it observes is unverified in this review.
|
||||
|
||||
---
|
||||
|
||||
## 4. Track Comparison
|
||||
|
||||
### Track A — Full EA-backend fake (M1–M7, playable FUT vs AI)
|
||||
|
||||
**What it delivers:** The FIFA 23 FUT hub loads from OpenFUT Core; Squad Battles matches play and reward economy items.
|
||||
|
||||
**Effort:** Research-grade. Minimum path: synthesize ONLINE_STATUS_EVENT (unknown format, 1–2 weeks of RE), then capture Fire2 frames (days once M2 is solved), then implement Blaze auth handlers (weeks), then implement FUT entry (weeks), then Squad Battles (weeks). Realistic minimum: 3–6 months of expert RE work.
|
||||
|
||||
**Proven support:** Hook loads and redirects correctly. TLS bypass patched. Core economy backend complete. Blaze framing code and TLS listener exist.
|
||||
|
||||
**Assumed:** Fire2 framing correct; component/command IDs discoverable from captures; FUT REST shapes close enough to community guesses; no additional undiscovered gates.
|
||||
|
||||
**Evidence for:** Architecture is coherent. The M1 finding (gate precisely named and decoded) was achieved cleanly. The in-process hook approach is validated.
|
||||
|
||||
**Evidence against:** M2 was attempted and failed with the in-process approach. The event-driven architecture adds a full EbisuSDK emulation layer before even one Blaze byte is seen. The live toolkit is exhausted (Path A verdict); Ghidra-level work on a 505 MB binary is required. Six capture files with zero bytes.
|
||||
|
||||
---
|
||||
|
||||
### Track B — Clean-room spec deliverable (M1–M5 documented)
|
||||
|
||||
**What it delivers:** A documented map of the connection gate, LSX event sequence, Blaze auth surface (transport, framing, gate conditions, component IDs, TDF schemas). Valuable as an archival/community artifact even if Track A stalls.
|
||||
|
||||
**Effort:** Medium. M1 is done. M2–M5 documentation emerges as a by-product of engineering work. The spec itself (writing) is lightweight; the engineering to produce the captures is the cost.
|
||||
|
||||
**Proven support:** M1 complete and documented. connection-gate-findings.md is already a high-quality spec artifact.
|
||||
|
||||
**Assumed:** Same as Track A for the unconfirmed values, but the spec can mark them `TODO/CONFIRM` rather than needing to implement them.
|
||||
|
||||
**Evidence for:** The clean-room constraint means a spec is the only artifact that can be safely published. connection-gate-findings.md shows this approach produces real value. B finishes even if A is never fully playable.
|
||||
|
||||
**Evidence against:** Track B alone doesn't produce a playable FUT; it is a foundation, not an end-user product.
|
||||
|
||||
---
|
||||
|
||||
### Track C — FLE Lua bridge (local-match path, skip the backend gate)
|
||||
|
||||
**What it delivers:** FIFA 23 career mode or Kick-Off with an OpenFUT club's players and squad loaded via FLE's in-memory DB API. No online gate, no Blaze, no TLS. Fully offline from day one.
|
||||
|
||||
**Effort:** Low-to-medium. FLE is already loaded in the normal launch path. Tools exist (`tools/squad-exporter/`, `tools/profile-exporter/`). Primary unknown is whether FUT-relevant DB tables are accessible.
|
||||
|
||||
**Proven support:** FLE Lua API exposes `GetDBTableRows` / `EditDBTableField` for career mode. `fifa23-startup-flow.md` confirms FLE injects at load. `fut-integration-options.md` documents the integration path in detail and rates this as the recommended option.
|
||||
|
||||
**Assumed:** FUT card/club/squad data has in-memory DB table representations that FLE can write. If FUT data is purely server-side (loaded from EA servers, not from the Frostbite DB layer), Track C produces no FUT simulation at all — only career mode player stats.
|
||||
|
||||
**Evidence for:** Career mode already works with FLE edits (community precedent). Tools are present and designed for this path. No infrastructure work needed.
|
||||
|
||||
**Evidence against:** FUT in FIFA 23 uses server-side data. The cards in a player's FUT club, the coins, the squad — these are fetched from `fut.ea.com` REST APIs, not from the Frostbite embedded DB. FLE's `GetDBTableRows` likely exposes base player stats tables but not FUT item tables. The crucial test (run `export_squad.lua` while in FUT mode) has never been done.
|
||||
|
||||
---
|
||||
|
||||
### Recommendation
|
||||
|
||||
**Start Track C immediately as a parallel, low-cost validation.**
|
||||
|
||||
Run `export_squad.lua` in FLE while inside the FUT hub (or attempting to enter it). If FUT tables appear in the export, Track C is viable and is the fastest path to something a user can interact with. This test takes one session and costs nothing.
|
||||
|
||||
Simultaneously, **continue Track A/B with the next concrete RE step:** synthesize the ONLINE_STATUS_EVENT push. The most actionable option is to run `origin_spy` logs from the current hook to see what LSX events fire during a session, then attempt to push candidate event XMLs via the bridge LSX server and watch whether the game advances. This is bounded, testable work that either unblocks M2 or produces the spec value for Track B.
|
||||
|
||||
**Do not abandon Track A/B for Track C** — they are complementary. Core is already built; the bridge is mostly built. The gap is purely the RE wall at M2.
|
||||
|
||||
---
|
||||
|
||||
## 5. Architecture and Provenance Sanity-Check
|
||||
|
||||
### Hook + Brain coherence
|
||||
|
||||
The CLAUDE.md bridge architecture diagram (hook intercepts ProtoSSL → plain localhost TCP → blaze_brain → Core) remains coherent. The M1/M2 findings revealed one additional layer (EbisuSDK LSX event) that must precede the Blaze connection. The bridge has been updated to handle LSX directly. The overall design is sound; the M2 blocker is an implementation gap (event synthesis), not an architectural flaw.
|
||||
|
||||
**One inconsistency to flag:** The hook's `lsx.rs` contains a complete in-process LSX emulator (AES-128-ECB, CRandom, all response builders), but the recv/send hooks that activate it are explicitly removed (`lib.rs`: "recv/send hooks removed — LSX is now handled by the native openfut-bridge LSX server"). This is dead code. The bridge's LSX server is the current path. The in-process lsx.rs should either be deleted or documented as a fallback; its presence is confusing.
|
||||
|
||||
### Clean-room status
|
||||
|
||||
No evidence of EA leaked source anywhere in the tree. All RE work is derived from:
|
||||
- Running the shipping binary and observing behavior (function return values, network traffic patterns)
|
||||
- Memory scanning of the live process (string search, xref, disasm of observed addresses)
|
||||
- Reading anadius's own compiled output (its exported symbols, its LSX XML format — which is anadius's own implementation, not EA's)
|
||||
- Community FUT API knowledge (mapper.rs endpoint paths — plausible but speculative)
|
||||
|
||||
The Blaze framing in `fifa-blaze/crates/blaze-proto/src/frame.rs` cites "Fire2 used by ME3, BF3, and most post-2012 titles" — this is sourced from public community documentation of those older titles, not from any leaked EA source. **Clean-room intact.**
|
||||
|
||||
The `AES_KEY` in the hook's lsx.rs (`[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]`) is a placeholder key used for the LSX session encryption. The real session key is derived from the challenge seed via CRandom — this algorithm was RE'd from anadius's own binary. No EA source required.
|
||||
|
||||
---
|
||||
|
||||
## 6. If You Read Only This
|
||||
|
||||
- **The project is blocked at M2.** FIFA 23 submits `GoOnline`, gets success, then waits for an async `ONLINE_STATUS_EVENT` push on the LSX socket that no current code ever sends. All six Blaze capture files are empty (0 bytes). No Fire2 frame has ever been decoded.
|
||||
|
||||
- **M1 is the only completed milestone.** The gate function (`GetInternetConnectedState @ anadius64.dll+0x27790`) is precisely named and its two-flag branch decoded. Everything after M1 is either blocked or not started.
|
||||
|
||||
- **The next concrete action** is synthesizing the ONLINE_STATUS_EVENT push XML and testing whether the bridge's LSX server can deliver it to the game. This is the single thing that unblocks all Blaze work.
|
||||
|
||||
- **Track C (FLE Lua) is untested but cheap to validate.** Run `export_squad.lua` while in FUT to find out if FUT DB tables are accessible. If yes, it is the fastest path to user-visible results. If no, it is ruled out with one session.
|
||||
|
||||
- **openfut-core is complete and ready** — 25 phases, 15 migrations, full economy REST API, passing tests. It is not blocking anything; it is waiting for the bridge to connect to it.
|
||||
@@ -1,93 +0,0 @@
|
||||
# Track C — FUT DB table viability test
|
||||
|
||||
**Status: PENDING — test has not yet been run.**
|
||||
|
||||
## What this test settles
|
||||
|
||||
Track C ("FLE Lua bridge") would inject OpenFUT club data directly into FIFA 23's
|
||||
in-memory Frostbite DB tables at runtime, bypassing the entire backend/Blaze stack.
|
||||
It is only viable for FUT (not just career mode) if FUT-specific tables — card
|
||||
inventory, squad composition with FUT fields, coins — are accessible in memory when
|
||||
the game is in the FUT area.
|
||||
|
||||
FUT data in online mode is fetched server-side from `fut.ea.com`. It is not known
|
||||
whether FIFA 23 mirrors any of this into the Frostbite in-memory DB that FLE
|
||||
can read/write. This test settles that question directly.
|
||||
|
||||
## Test procedure
|
||||
|
||||
**Prerequisites:**
|
||||
- FIFA 23 launched normally via umu-run/Steam
|
||||
- FLE (FIFA Live Editor) injected and active (normal launch path)
|
||||
- EAAC in offline/neutralized state
|
||||
- Game navigated as deep into FUT as possible (FUT hub if reachable; otherwise the
|
||||
furthest FUT screen before the gate blocks it)
|
||||
|
||||
**Run the exporter:**
|
||||
1. In FLE's Lua Engine, open and run `tools/squad-exporter/export_squad.lua`
|
||||
(full path on the Windows side: `C:\<game>\openfut_squad_export.json`)
|
||||
2. Wait for the MessageBox "Done! N players, M teams." or "ERROR writing..."
|
||||
3. Retrieve the output file from the Wine prefix:
|
||||
`~/Games/umu/fifa23-tools/drive_c/FIFA 23 Live Editor/openfut_squad_export.json`
|
||||
(or wherever `C:\FIFA 23 Live Editor\` maps in the active prefix)
|
||||
|
||||
**What to inspect in the output:**
|
||||
- `all_db_tables` array — the complete list of table names visible to FLE right now
|
||||
- `fut_tables` object — any table whose name contains `fut`, `club`, `pack`, `item`, or
|
||||
`market` (the script auto-extracts these)
|
||||
- `is_career_mode` — confirms whether FUT or career mode was active
|
||||
|
||||
## Classification criteria
|
||||
|
||||
### "FUT tables present"
|
||||
|
||||
`fut_tables` is non-empty AND contains FUT-specific fields beyond base player stats:
|
||||
- e.g., `fut_items` with card-type / rating / chemistry fields
|
||||
- e.g., a squad table with FUT formation / chemistry / loan-flag fields
|
||||
- e.g., a coins or points balance field
|
||||
|
||||
**Verdict:** Track C is viable for FUT. Fastest path to user-visible results.
|
||||
|
||||
### "only base player tables"
|
||||
|
||||
`fut_tables` is empty (no `fut_*` / `club_*` / `item_*` / `market_*` table names found
|
||||
in `all_db_tables`), OR those tables exist but contain only base player attributes
|
||||
(OVR, potential, position, pace, …) — the same fields visible in career mode.
|
||||
|
||||
**Verdict:** Track C cannot produce FUT. It could at most provide a custom Kick-Off or
|
||||
career-mode match with players sourced from OpenFUT Core. FUT items and coins exist
|
||||
only on EA's servers (not in the in-memory DB in offline mode).
|
||||
|
||||
### "FUT area unreachable to test"
|
||||
|
||||
The connection gate blocked entering FUT deeply enough for FUT tables to be populated.
|
||||
Record which tables were visible and at what screen the test was run.
|
||||
|
||||
**Verdict:** Retest after M2 is unblocked, OR test with `TLS_ENABLED=false` bridge
|
||||
handling the entry check stub.
|
||||
|
||||
## Results
|
||||
|
||||
*(To be filled in after the test is run.)*
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Date run | — |
|
||||
| FIFA screen at test time | — |
|
||||
| `is_career_mode` | — |
|
||||
| Total tables in `all_db_tables` | — |
|
||||
| FUT-specific table names found | — |
|
||||
| Key FUT fields present | — |
|
||||
| **Classification** | **PENDING** |
|
||||
|
||||
## Honest prior
|
||||
|
||||
`fut-integration-options.md` rates this as the recommended path and lists `fut_clubs`,
|
||||
`fut_items`, `fut_squads` as "expected" tables. However those expectations are based on
|
||||
analogy with career mode (which does store club/squad in the DB). FUT's data model is
|
||||
architecturally different — it is account-bound server-side. The expectation may be
|
||||
wrong. This test is the oracle.
|
||||
|
||||
The `export_squad.lua` script checks `GetDBTablesNames()` exhaustively (not just
|
||||
assumed names), so it will surface any FUT tables that actually exist, regardless of
|
||||
what name they use.
|
||||
Reference in New Issue
Block a user