# 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 `" "` at `:3732`, and the response line `" -> "` 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//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": [], "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":,"finalFunds":}]`, `extPrice:{finalPrice/originalPrice:{amount:,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.