Files
OpenFUT/docs/evidence/STORE_TILE_6C.md
T
funman300 e8ee6c34e7 docs(fifa17): record empty My Packs client contract
Full investigation record for bug 6c: baseline + Experiments A/B/C', Candidate F (contradicted), the explicit active-placeholder selection test, the minidump-confirmed CardsDLL crash, and the P2 decision. Marks ROOT CAUSE ESTABLISHED and documents the known UX limitations and the client-side follow-up.

Files: docs/evidence/STORE_TILE_6C.md, docs/evidence/FIFA17_EMPTY_MYPACKS_CLIENT_CONTRACT.md, the four genuine /store/purchasegroup captures (baseline, mypacks70, empty_no_sentinel, active_placeholder), and docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md (client-side design/research).
2026-08-13 01:08:47 +00:00

54 KiB
Raw Blame History

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_IDscreen+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 :3754response 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_DISPLAYGROUPdisplayGroup = {"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 tokensmypacks, 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/credits200 {"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.luaResolutionWidth = 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: YESStore.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 /hub00:21:07 GET /user/credits00: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)=Noneif not pack: return 200, {} (no open_pack, no coin change).
  • purchased_items (POST, :3494-3496): pack_by_id(65534)=Nonereturn 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 refutedstate 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_buy200 {}; purchased_items200 {"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".
  2. Stays in My Packs: yes. 4. After closing the dialog → returns to My Packs.
  3. Then navigates back to the FUT Hub successfully. 6. No crash. 7. No spinner.
  4. 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.