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).
54 KiB
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/purchasegrouphandler emits PACK definitions only — nocardsubtypeid/carddbid/cardassetidanywhere 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.valuetoken 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 vialog(),utas_server.py:59-62; each request logs"<VERB> <path>"at:3732, and the response line" -> <code> <body[:200]>"at:3754— response body truncated to 200 bytes, so the full JSON body is NOT in the log). - Endpoint / path matcher:
GET /ut/game/fifa17/store/purchasegroup/all?ppInfo=true(OBSERVED historically in the log; route regex/store/purchasegroup,utas_server.py:1215). - Capture marker (UTC, set immediately before the manual test):
2026-08-12T23:54:01Z(saved to/tmp/store_capture_marker.txt; 0 log lines after it at set-time → clean boundary). - Log command to isolate the manual action:
docker logs --since 2026-08-12T23:54:01Z --timestamps openfut-fut-backendthen locate the firstGET .../store/purchasegroupline 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 staticPACK_CATALOG(fut_store.py:820) + liveSTORE.unopened_packs()(from/stateprofile) under fixed flags (STORE_DISPLAYGROUP=ON,STORE_GROUPID=OFF,FUT_PRICE_PROBE=OFF). Plan: reconstruct the exact body from the running/appcode + live/stateprofile, then verify itsjson.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 is10.10.0.105. - Backend under test: Docker container
openfut-fut-backend(imageopenfut-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 noFUT_STORE_*vars.entrypoint.shlaunchespython3 -u utas_server.pyfrom/app/toolswith extra envFUT_TRADING=1 FUT_PILESIZES=1 FUT_TRADEABLE=1 FUT_DISCARD_TABLE=1 FUT_DISCARD_SEND=1— again noFUT_STORE_*.
- The running store code is
/app/tools/utas_server.py. Extracted read-only viadocker cp openfut-fut-backend:/app/tools /tmp/app-toolsand confirmed byte-identical (sha256) to the repo copyfifa17-recon/tools/utas_server.py(both 3765 lines) andfifa17-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:
- Endpoint.
GET ut/<sku>/store/purchasegroup/...—FutStoreGetPackTypes(client deser root0x1801234e0). (utas_server.py:3408-3409docstring.)- Route table entry:
(re.compile(r"/store/purchasegroup"), lambda m,h: store_catalog(h))atutas_server.py:1215. - Sibling store routes:
/store/transaction -> store_buy(h)(:1216, the BUY); bare/store(\?|$) -> (200,{"result":"SUCCESS"})eligibility gate (:1221).
- Route table entry:
- Handler.
store_catalog(h)(utas_server.py:3408-3447).- Iterates
PACK_CATALOG(non-ownedOnlypacks) →_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
mypackssentinel (id 65534) soGOTO_STORE_MYPACKresolves (:3428-3446). - Returns
200, {"purchase": [<pack bodies>], "timestamp": 1596326400}(:3447).
- Iterates
- 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}wherecategory ∈ {special, gold, silver, bronze}chosen fromp["specialChance"]/p["gold"](:3337,:3373-3379), and ifSTORE_GROUPIDalsodisplayGroupAssetId = p["id"](:3403-3404).
- owned pack →
- Data source(s).
PACK_CATALOG— a hardcoded list of 3 pack dicts ({id,name,price,count,gold,tiers,specialChance}) atfut_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()readsunopenedPackIds,fut_store.py:619-621).
- 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_bodymaps a pack to a category token viaspecialChance>=1.0 -> special, elsegold -> gold,p.get("silver") -> silver, elsebronze(:3373-3379).- The tile caption/category is
displayGroup.value;descriptioncarries 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 nocardsubtypecolumn at all (Task 1: badges/stadium/kit/logos/balls have empty subtype sets).resourceId/assetId=carddbid, andcardassetid= 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) ascardsubtypeid.
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.pyor the store path. (Grep: nomiscfamily, 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 inFAMILIES(onlyfcc_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_staffon/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) |
:975STORE_DISPLAYGROUP = os.environ.get("FUT_STORE_DISPLAYGROUP", "1") == "1":980STORE_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_bodyreads the resulting module constants (:3337,:3403), neveros.environat request time. - Repo-wide there is no
importlib.reload, nosignal/SIGHUPhandler, and no per-request environ re-read for these flags. (The only runtime-refreshable feature isFUT_ID_SWEEP, which re-reads a fileSWEEP_FILE,:1965-1966— unrelated.) :3250documents 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(Host10.10.0.120:8099,User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows),X-UT-SIDpresent). Preceded at 23:54:43 byGET /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/stateprofile, under the live flags (confirmed in-process:STORE_DISPLAYGROUP=True,STORE_GROUPID=False). Itsjson.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 atdocs/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 answered200).
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 epilogue0x18013badcpushes every parsed pack unconditionally — no drop predicate in the parse path. So a200with clean JSON cannot be rejected by the deserializer. - Store-level "not available" (
FUT_CatalogNotAvailable, msg0x7550) comes from downstream client gates (OPENCODE_ENDPOINT_PROMPT.md:120-131): (1) resolutionGetSystemMetrics≤1024×768, (2) store-data-model load status0x180013cf0, (3) Blaze purchase-config flagsIS_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): fieldsstate/saleType/quantity/ purchaseLimit/purchaseCount/start/endare parsed and copied to the tile, but the predicate that greys/blocks a tile "is in the packed exe and unread." WhetherpurchaseLimit+purchaseCountgreys 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_CatalogNotAvailableandCARDS_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.valuetoken): 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:
- Unsupported
displayGroup.valuein the response? No — all four tokens (bronze/gold/special/mypacks) are recognized. (OBSERVED) - Which pack got it? None. (OBSERVED)
- 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)
- Where does the backend assign the token?
_pack_body:3373-3379maps each pack tospecial/gold/silver/bronzefromspecialChance/gold; owned/sentinel →mypacks(:3336). All canonical. (OBSERVED) - Is
displayGroup.valuesufficient to explain the bug? No. The response is clean; the failure is a downstream client/packed-exe gate, not a token. (INFERRED) - Is the server emitting a value FIFA demonstrably cannot understand? No. (OBSERVED)
- Narrowest likely fix (REPORT ONLY — not implemented): The single anomalous,
server-controllable element is the inactive empty
mypackssentinel emitted whenunopenedPackIds == [](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 aninactivepack 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
- 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).
- Does the store, with
unopenedPackIds == [], land on / auto-select the inactivemypackssentinel? (HYPOTHESIS §8.7; unproven without client-side observation.) - 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.)
- Does
purchaseLimit:0/purchaseCount:0grey a tile? Existing RE lists this as an OPEN question; would need a controlled experiment. (UNKNOWN) - Is bug 6c ("unknown tile") a stale symptom from before
STORE_DISPLAYGROUPbecame the defaultON? 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-2andcard1-HDMI-A-1bothconnected, native mode 2560x1440; compositor KDEkwin_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/Xauthorityinside the game namespace).
Is FIFA rendering above 1024x768? YES (1280x720). Resolution hypothesis eliminated for this reproduction.
11. Sentinel 65534 provenance (Phase 3)
- Basis: OpenFUT INVENTION (OBSERVED).
65534(0xFFFE) appears nowhere in any EA capture or recon note — repo-wide it exists only inutas_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. - Known-good EA capture of EMPTY My Packs: UNKNOWN — none found. All recon
My-Packs analysis is client-side RE (
FUN_1800150d0filtersdisplayGroup.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. - Known-good response with ≥1 unopened pack: UNKNOWN for EA. OpenFUT's own
seed grants reward pack id 70 (
_new_profileunopenedPackIds:[70],fut_store.py:360), but that is an OpenFUT synthetic grant, not an EA capture. - 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.
- Evidence for the
inactiverepresentation: UNKNOWN / HYPOTHESIS. The claim "state != active keeps it out of the visible row list" (:3432) is an unverified author assumption; no RE showsstate:"inactive"hides a pack from selection. If FIFA does NOT hide it, the solemypacksentry 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-
ownedOnlycatalogue packs (ids 1,5,6,7) via_pack_body. - For each id in
visible_unopened_packs()(=STORE.unopened_packs()+_OPENED_PACK_GRACE) that resolves inPACK_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 inPACK_CATALOG, appends tounopenedPackIds, persists; reverts viaconsume_unopened_pack(:623-633). Modifies only profile state; cleanly reversible. - BUT restart IS required to take effect.
Store.load()cachesself._p(:370-372); no route callsgrant_unopened_packorselect_account, andselect_accountonly 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 cachedunopenedPackIdsuntil 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
unopenedPackIdsto[70](edit…/state/accounts/33068179/fifa17_profile.json, or callSTORE.grant_unopened_pack(70)), store code/config unchanged. - Restart required? YES — profile cache (above). Profile-only; no code edit.
- Rollback: set
unopenedPackIdsback to[](orconsume_unopened_pack(70)), restart. (Also restorenextItemId/coins only if a pack is actually opened — the grant alone touches onlyunopenedPackIds.) - Expected
purchasegroupdifference: sentinel 65534 GONE; instead one active pack id 70 in themypacksgroup (state:active,unopened:true); hub/credits reportrecoveredPacks: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), whenunopened_packs()is empty, do NOT append pack 65534 (emit nomypacksentry). Code change ⇒ restart. NOT authorized. Narrowest patch: guard/remove theif not owned_ids:sentinel block. - Purpose: distinguishes "the inactive sentinel is selected and fails" (A already
tests the inverse) from "an absent
mypacksgroup 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
mypacksgroup represented differently (e.g. present-but-not-a-pack, orstateother thaninactive), 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.pathin-process =/state/accounts/33068179/ fifa17_profile.json, coins 29876776 matching the live/user/credits). unopenedPackIds == []. Original SHA-25639bb3e833fa55287d8516815ba3a717b41c0f0c7a7c41d20503f3a55c65cc6e7. 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 keyunopenedPackIds([] → [70]), all other 24 keys identical. Modified SHA-2562b5760baa265f320904de2d23fd6ab374733efe74cb0756c3be39c083bce4ac8. (Used the direct edit rather thangrant_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 (perentrypoint.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/itemsin 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 atdocs/evidence/store_purchasegroup_capture_mypacks70_2026-08-12.json.
14.5 Client-observed behavior (operator report)
- Store remains open: YES.
- "The pack you've selected is currently not available": NO (gone).
- My Packs category / unopened reward pack visible: YES.
- 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
unopenedPackIdsAND suppress the sentinel somypackshas 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 (still2b5760ba…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 anymypacksentry (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.pyrestored from backup → SHA-256c89d43ea…== pre-experiment (byte-identical); theif 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.pynever edited (git diff empty, SHA-256c89d43ea…). 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):
- 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
mypacksis 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). - Change what
store_catalogadvertises so FIFA never lands on / requires amypacksgroup 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, {}(noopen_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, noopen_pack, noconsume_unopened_pack).open_pack/consume_unopened_packare 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)
- No crash (game launched to hub).
- No "pack not available" dialog.
- Store remains open.
- My Packs tab not shown while inside the Store (Browse Packs).
- 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).
- 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
{}/ stalelast_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:
- Dialog: YES. 2. Exact text: "This pack is no longer available".
- Stays in My Packs: yes. 4. After closing the dialog → returns to My Packs.
- Then navigates back to the FUT Hub successfully. 6. No crash. 7. No spinner.
- 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.