Land the client-side empty-My-Packs resolver evidence and the session-stability invariant established by the F3/R1 experiments. - PART III (F3, CONFOUNDED CRASH): a mid-process sentinel -> no-sentinel flip left a stale POSITIVE My-Packs ordinal that still took the resolve branch and crashed at 0x180014882. Preserved verbatim (not a guard failure). - PART IV (R1, SUCCESS): backend set no-sentinel first, then a FRESH FIFA process; genuine purchasegroup response ids [1,5,6,7] is byte-identical to the F3 capture, so client process lifetime is the only changed variable. Store opens on Browse Packs, no crash, no dialog. Guard PROVEN on the tested build. - New INVARIANT: empty-My-Packs capability MUST be session-stable -- the server must not switch a running client between sentinel-present and sentinel-absent for the My Packs group within one FIFA process, because the client caches the group ordinal and a stale positive ordinal still crashes the resolver. - Both no-sentinel captures kept: client_guard (F3) and freshretest (R1). Backend P2 active-sentinel (65534) remains production default; no capability handshake is implemented yet.
28 KiB
FIFA 17 — Empty "My Packs" Client Contract (store/purchasegroup)
STATUS (2026-08-13): ROOT CAUSE ESTABLISHED; P2 backend compatibility workaround IMPLEMENTED. Root cause: FIFA 17's Store/Scaleform path resolves the
mypackscategory even with zero unopened packs, and CardsDLLFUN_1800147f0assumes the resolved group is non-null (crash if absent). Backend decision: P2 — emit an active non-openable syntheticmypacksplaceholder (id 65534) whenunopenedPackIds == [](fifa17-recon/tools/utas_server.pystore_catalog; testsfifa17-recon/tools/test_empty_mypacks.py). Crash-safe + economy-safe; known UX limitations (fake tile, click-dialog, Browse→My-Packs nav quirk) are Scaleform-driven and require the client-side fix indocs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md. This is a FIFA-17-specific compatibility shim, NOT an EA-authentic representation, and is confined to the FIFA-17 adapter/backend (NOT OpenFUT Core).
Evidence labels: OBSERVED (runtime capture / crash dump / already-decompiled RE quoted in-repo), INFERRED, HYPOTHESIS, UNKNOWN. No server behavior is changed by this document; it is analysis only.
Binaries (hashes verified 2026-08-13 on .105):
CardsDLL_Win64_retail.dll SHA-256 4706a881ae1fc7b5769fd810b25a868d29d2b16a8e65a7513436327ef645573c
(load base in the crash dump 0x00006FFFFC120000; RE-space base 0x180000000).
FIFA17.exe SHA-256 29c31cef12b0c3c2a7305220617c7b4fa139ab76b8c857851bdbe88987962899 (packed).
1. Question
How must the server represent an account that owns zero unopened packs in
GET /ut/game/fifa17/store/purchasegroup/all?ppInfo=true so that FIFA 17 neither
(a) crashes nor (b) shows "The pack you've selected is currently not available",
without granting the user a real/openable pack and without breaking the normal
bronze/gold/special store? The current OpenFUT answer (a synthetic inactive mypacks
sentinel) only downgrades a crash to a dialog; it is not the correct contract.
2. Established experimental behavior (OBSERVED)
Profile-state ladder, all with normal packs 1/5/6/7 unchanged:
profile unopenedPackIds |
mypacks group in response |
client outcome |
|---|---|---|
[] (baseline) |
one inactive sentinel pack 65534, empty description |
"pack not available" dialog → FUT Hub |
[70] (Exp A) |
one active real owned pack 70 |
store works; My Packs visible |
[] + sentinel suppressed (Exp B) |
no mypacks group at all |
client CRASH |
Captures: store_purchasegroup_capture_2026-08-12.json (baseline),
…_mypacks70_2026-08-12.json (A), …_empty_no_sentinel_2026-08-12.json (B).
Details in STORE_TILE_6C.md §14-§15.
3. Existing RE evidence (from docs/plan-2026-08-05-store-subsystem.md, decompiled)
All addresses RE-space (CardsDLL base 0x180000000):
FUN_18013af30— per-element pack deser; eachpurchase[]entry → a0x158wire record.displayGroup.value(atom 0x377) → record+0x00(ctor default the literal"unknown").displayGroup.priority(0x250) →+0x34. (store-subsystem §"wire record", :996.)FUN_1800150d0— group builder. Walkspurchase[]in array order; for each pack, finds-or-creates a0x108display group by exact strcmp ofdisplayGroup.valueagainstgroup+0x70(FUN_180014380). New group (FUN_180012950):+0x00= 1-based ordinal (groups-so-far+1),+0x100= priority,+0x104= (value == "mypacks"),+0x40= vector of0x1a8tile models. Pack→tile viaFUN_18002c3c0. (:152-161, :1042-1049.)FUN_1800147f0— group resolver/renderer, called asFUN_1800147f0(model, screen+0x290, dataProvider, 0, 0)fromFUN_18007dab0.screen+0x290 == 0→ "list the group tiles" (FUN_180014610); any other value →FUN_180014420, which exact-matchesgroup+0x00(the ordinal) and returns NULL on a miss, after whichFUN_1800147f0dereferences[RAX+0x40]with NO guard ("checked in raw disassembly … a real absence"). "The only legal category values are 0 and the ordinals 1..N." (:163-169, :1051-1054.)FUN_180014580— the six-tab bar: switch 0..5 over the hardcoded lowercase literalsmypacks, points, bronze, silver, gold, special.FUN_18007e5e0gives each panel aPANEL_ID= the matching group's ordinal, or HIDES the panel if no matching group;FUN_18007df60publishesMYPACK_/…/SPECIAL_CATEGORY_ID. (:202-206, :1058-1062.)
4. Store parser code path (OBSERVED, decompiled)
HTTP 200 {"purchase":[...]} (server: store_catalog / _pack_body)
→ per-element deser FUN_18013af30 → 0x158 wire records
→ FUN_1800150d0 → 0x108 display groups (by displayGroup.value),
tiles (0x1a8, FUN_18002c3c0) into group+0x40
→ render FUN_18007dab0 → FUN_1800147f0(model, screen+0x290, …)
screen+0x290 == 0 → FUN_180014610 list this group's tiles
screen+0x290 == N>0 → FUN_180014420 exact-match ordinal; NULL on miss
→ [RAX+0x40] dereference (NO NULL GUARD) ← crash site
5. My Packs group construction (OBSERVED)
A mypacks group exists iff at least one purchase[] entry carries
displayGroup.value == "mypacks" (the group is derived from packs; there is no
independent group object on the wire). Its ordinal is its 1-based creation position
in array order; group+0x104 is set because the value is "mypacks"; its tiles live
in group+0x40. Consequently the server cannot emit an "empty mypacks group"
via purchase[] — removing the pack removes the group entirely.
6. Default / selected group logic (partly UNKNOWN)
FUN_1800147f0resolves whatever category ordinal it is handed viascreen+0x290; legal values are0(list tiles) and1..N(existing ordinals). A value that is not an existing ordinal (e.g.-1for a hidden/absent panel) →FUN_180014420NULL → crash. (OBSERVED via §8 crash + RE.)- Whether the store defaults to / auto-resolves the
mypackscategory on open, and why it does so even when the unopened count is 0, is UNKNOWN — the store-screen controller and default-tab selection are Scaleform/packed-FIFA17.exe ("Which tile the movie thinks you clicked | CLIENT | Scaleform, unread", store-subsystem :695). Experiment B proves that in this configuration the client DID resolve amypacksordinal that did not exist (it crashed), so the store is reaching the My Packs category with zero owned packs. Root of "why" = UNKNOWN.
7. Pack availability predicate (UNKNOWN)
The predicate that turns the baseline inactive sentinel into "pack not available"
(while active pack 70 passes) is in the packed FIFA17.exe and unread
(plan-2026-08-05-pack-opening.md:553: state/saleType/quantity/purchaseLimit/ purchaseCount/start/end are parsed and copied to the tile, "the predicate that greys
a tile is in the packed exe and unread"). Candidate deciding fields, from the
baseline↔A diff (INFERRED, unproven): state (inactive→active) and/or
unopened (false→true). The exact field is UNKNOWN.
8. Experiment B crash analysis (OBSERVED)
Minidump CrashDump_…18.21.08…dmp (the only crash in the hour; minute :21 matches
the 00:21:07 store request; SHA-256 fbddda18…), parsed:
- Exception:
0xC0000005ACCESS_VIOLATION, access type READ, faulting VA0x0000000000000048. - Faulting instruction:
CardsDLL_Win64_retail.dll + 0x14882→ RE-space0x180014882=0x92bytes intoFUN_1800147f0(entry0x1800147f0). - Interpretation (OBSERVED crash ⋂ decompiled RE): the group pointer returned by
FUN_180014420was NULL (nomypacksgroup present withunopenedPackIds==[]and the sentinel suppressed), andFUN_1800147f0dereferenced[NULL+0x48]→ read of address0x48→ access violation. This is exactly the "no null guard" branch the RE flagged (RE said[RAX+0x40]; the actual faulting offset is+0x48, same member region — the group struct's+0x40vector accessed via a+0x48field). - Note: the pre-built Ghidra project and
/tmp/fut/cardsdll.dllwere absent on.105(tmp cleared); confirmation used the OBSERVED crash dump + the previously-decompiled RE rather than a fresh (expensive) re-analysis. CardsDLL is unpacked, so this code is statically readable if a fresh project is ever needed.
9. Empty My Packs client contract (the answer, as far as evidence allows)
- The client has NO guard for a missing selected group. If the store resolves the
mypackscategory and nomypacksgroup exists, it null-derefs and crashes. (OBSERVED.) - A
mypacksgroup can only exist if apurchase[]entry carriesdisplayGroup.value=="mypacks". (OBSERVED.) There is no wire representation of an "empty group." - If the
mypacksgroup's selected pack is not a valid/active pack, the client shows "pack not available". (OBSERVED baseline vs A; deciding field UNKNOWN, §7.) - Therefore, under the current client behavior, a zero-unopened-packs account is
only cleanly handled when the
mypacksgroup contains a valid active pack (Exp A). Whether an active-but-non-openable placeholder would also satisfy the availability predicate is UNKNOWN (depends on §7). - The correct retail-EA representation of zero unopened packs is UNKNOWN. It is
NOT "omit the group" (crash) and NOT "inactive placeholder" (dialog). It is most
likely one of: (i) the retail store does not auto-select My Packs when the unopened
count is 0 (a client/Scaleform decision, possibly gated by a count the server sets),
or (ii) retail sends a
mypacksentry the client treats as an empty-but-valid state via a field we have not identified. Neither is established.
10. Candidate server representations
| # | Candidate | Client evidence | Expected behavior | Confidence | Safe to test? |
|---|---|---|---|---|---|
| A | No mypacks pack, no mypacks group |
Exp B crash (0x180014882, [NULL+0x48]) |
CRASH | OBSERVED | Already tested — reject |
| B | mypacks group present but zero packs |
Not representable via purchase[] (groups derive from packs, FUN_1800150d0) |
UNKNOWN | INFERRED-not-representable | No server mechanism |
| C | Inactive placeholder pack (current sentinel) | Baseline dialog | "pack not available" → Hub | OBSERVED | Already tested — reject |
| C′ | Active placeholder pack, id absent from PACK_CATALOG (so open/buy handlers reject it) |
Exp A shows an active mypacks pack works; sentinel id 65534 is already rejected by open/buy (not in PACK_CATALOG) |
Group resolves (no crash); MIGHT pass availability (no dialog) while remaining non-openable → no free pack | HYPOTHESIS | Yes — code change, restart; economy-safe (non-openable) |
| D | Hide My Packs when unopened count == 0 | FUN_18007e5e0 hides a panel with no group, but the store still resolved mypacks at count 0 (Exp B crash) |
Hiding via absence CRASHES; a client count-gate is Scaleform/unknown | UNKNOWN | Not server-controllable as far as known |
| E | Different default category when count == 0 | Default-tab selection is Scaleform/packed | UNKNOWN | UNKNOWN | Not server-controllable as far as known |
| F | A count/quantities field (e.g. ut/v2/store FutStorePackQuantities, or userInfo.unopenedPacks) that suppresses the My Packs auto-select |
eligibility gate exists (ENDPOINT_MAP.md:60); relationship to My Packs default UNKNOWN |
UNKNOWN | HYPOTHESIS | Read-only RE first |
11. Recommended next controlled experiment
Experiment C′ (economy-safe placeholder). Keep unopenedPackIds == []; change the
synthetic sentinel 65534 ONLY in state (and, if needed, unopened) so the
mypacks group's single tile is active — but leave its id 65534 absent from
PACK_CATALOG so store_buy/open_pack/consume_unopened_pack still reject it
(no pack can be opened → no free pack, no economy change). Observe whether the
store then opens without the "pack not available" dialog (would identify state as
the availability field and give an economy-safe fix), or still shows the dialog
(implicating another field / packed predicate).
- Requires a temporary code change to
store_catalog(sentinel construction) → restart. Same experiment discipline as Experiment B (patch container copy, capture, revert, restart). Economy-safe because the placeholder remains non-openable. - If C′ still fails, escalate to read-only RE of the availability predicate / the count-gated default-tab hypothesis (candidate F) before any further change.
12. Open questions
- Exact field that flips the sentinel from "pack not available" to acceptable
(
state?unopened? another). UNKNOWN — packed predicate. (Exp C′ targets this.) - Why does the store resolve/select
mypackswith zero owned packs? Is there a server-settable count that would stop it? UNKNOWN — Scaleform/packed. - Does retail FIFA 17 ever present an empty My Packs, and how? No capture on record.
- Is an active-but-non-openable placeholder (C′) accepted by the availability predicate? HYPOTHESIS — untested.
Permanent-fix requirements (Phase 11 — REPORT ONLY, not implemented)
A correct permanent fix MUST satisfy ALL of:
- Zero unopened packs must NOT grant the user a free pack.
- No synthetic openable reward may be created (any placeholder must be rejected by
store_buy/open_pack/consume_unopened_pack). - Client must not crash (a resolvable
mypacksgroup must exist, OR the client must be kept from resolvingmypackswhen empty). - Client must not show "The pack you've selected is currently not available".
- Normal Bronze/Gold/Special store categories must still work unchanged.
- When a genuine unopened pack exists, My Packs must continue to work (Exp A).
- Profile/economy semantics must remain correct (no coins/nextItemId/inventory drift).
Nothing implemented. The evidence favours investigating an economy-safe active placeholder (C′) and/or the count-gated My-Packs default (F); it explicitly does NOT support "grant pack 70 whenever My Packs is empty".
UPDATE after Experiment C′ (2026-08-13) — active non-openable placeholder tested
Executed C′: sentinel 65534 state "inactive"→"active" only; unopenedPackIds==[];
65534 kept out of PACK_CATALOG. Full record in STORE_TILE_6C.md §16. Capture:
store_purchasegroup_capture_active_placeholder_2026-08-12.json (C′-vs-baseline JSON
diff = only 65534.state).
Resolves §7 (availability predicate), partially: state DOES participate
(OBSERVED). state:"active" removed the "pack not available" dialog while the group's
existence still prevented the crash. So the earlier §7 "deciding field UNKNOWN" is
updated: state (inactive vs active) is (at least) a deciding field for the
dialog. unopened was NOT varied and remains untested. The full predicate may still
involve other fields, but state alone flips dialog→no-dialog.
Updated candidate table verdict:
- C′ (active placeholder, non-openable): SUPPORTED with UX caveats — best option so
far, but NOT adopted. No crash, no dialog, store usable, and no automatic
transaction/open for 65534 (only a routine boot
TRANSACTIONCANCELno-op). Caveats (OBSERVED): (1) the placeholder renders as a visible empty pack tile ("0 items, 0 bronze, 0 rares", no cover) that a user could try to open (server-safe: opening 65534 → no-op{}/stalelast_pack, no value — §16.1); (2) navigation gate: from the Store "Browse Packs" entry the Bronze/Gold/Special categories are not reachable until "My Packs" is opened first (not present with a genuine owned pack, Exp A). - A (real active pack 70): works cleanly but grants a real openable pack → economy risk; rejected as the permanent fix.
- Candidate F (count-gated My-Packs default) gains weight: C′'s visible-empty-tile
and Browse-Packs navigation gate suggest the client is being pushed to resolve/enter
My Packs when it should not with zero packs. If a server-settable count (e.g.
userInfo.unopenedPacks/ut/v2/storequantities) suppresses the My-Packs default/tile, that could remove both the crash risk and the empty-tile artifact without any placeholder. UNTESTED.
Permanent fix: still NOT established. Even though C′ is the first
crash-free/dialog-free representation, the empty-tile UX + navigation gate + the
untested "explicit placeholder selection" behavior bar adoption. Required next steps
(design/authorize separately): (a) controlled test of explicitly focusing/opening the
active placeholder; (b) investigate candidate F (count-gated My-Packs) to avoid a fake
tile entirely. Do NOT adopt state:"active" or "grant pack 70" as the fix on current
evidence.
Candidate F — Count-Gated My Packs Navigation (READ-ONLY investigation, 2026-08-13)
Question: can the server make FIFA decide not to resolve/default into My Packs when the account owns zero unopened packs (avoiding any placeholder)?
1. Server-sent unopened-pack signals (inventory, OBSERVED code)
| field / endpoint | source | value source | when sent | client consumer | conf |
|---|---|---|---|---|---|
userInfo.unopenedPacks.recoveredPacks (via userMassInfo) |
utas_server.py:409-414 |
len(unopenedPackIds) |
boot massinfo; only if count>0 or _UI∈{packs,full} (default _UI=roster → omitted at 0) |
hub unopened-pack model / My Packs badge (FUN…vtbl[0x4e0], pack-opening RE) |
OBSERVED (code) |
/user/credits .unopenedPacks.recoveredPacks |
utas_server.py:3542-3545 |
len(unopenedPackIds) |
on credits fetch; only if count>0 | My Packs badge / CentralUnclaimedPack hub tile | OBSERVED (code+capture) |
/hub body |
utas_server.py:1449-1453 |
— | hub load | — (no pack count present) | OBSERVED |
profile unopenedPackIds |
fut_store.py |
account state | internal | not wire-visible directly | OBSERVED |
pileSize/store quantities (ut/v2/store FutStorePackQuantities) exist as an |
|||||
eligibility gate (ENDPOINT_MAP.md:60) but were never requested in any capture |
|||||
(8h logs); they carry a store-open result, not a My-Packs count. |
2. Pre-store request sequence (OBSERVED, captures)
Boot → accountinfo → /ut/auth → settings → phishing → match/reset → userMassInfo → PUT store/transaction/0 (TRANSACTIONCANCEL→{}) → /hub → clientdata → /user/credits → GET /store/purchasegroup/all. The only pack-count-bearing responses before
purchasegroup are userMassInfo(userInfo) and /user/credits.
3. Baseline([]) vs Experiment A([70]) pre-store diff (OBSERVED, captures)
The single profile change [] → [70] altered exactly one pre-store wire signal:
/user/credits:[]→ nounopenedPacksmember (C′ capture, all 3 fetches:{"credits":…,"currencies":[…]});[70]→"unopenedPacks":{"preOrderPacks":0, "recoveredPacks":1}(A capture line 25). OBSERVED.userInfo.unopenedPacks: same pattern (present at[70], omitted at[]with_UI=roster). INFERRED from code; A-capture credits corroborates.- No other pre-store field changed.
Candidate signal:
Candidate: unopenedPacks.recoveredPacks (count)
Endpoint: /user/credits and userMassInfo(userInfo)
Baseline([]) value: ABSENT (i.e. zero)
Experiment A([70]) value: {preOrderPacks:0, recoveredPacks:1}
Source: utas_server.py:3542-3545 / :409-414 (= len(unopenedPackIds))
Client-visible before purchasegroup?: YES
Confidence: OBSERVED that it differs; its CONTROL over My-Packs nav = see §9
Key point: this count is already CORRECT — it reports zero (absent) when the account is empty. OpenFUT is not misreporting a nonzero pack count.
4. Navigation / client call path (OBSERVED, decompiled RE)
FUN_18007dab0 → FUN_1800147f0(model, screen+0x290, …). screen+0x290==0 lists
group tiles; else FUN_180014420 exact-matches the group ordinal (NULL on miss →
[NULL+0x48] crash). screen+0x290 is written in exactly two CardsDLL sites: the
screen ctor FUN_18007d1a0 writes 0, and FUN_18007e7f0 case 0x7551 copies
the Flash movie message field CATEGORY_ID verbatim into it
(store-subsystem :172-175, :1055-1056). So the resolved category is chosen by the
Scaleform movie, not by any server response field.
5. GOTO_STORE_MYPACK analysis (OBSERVED, RE)
GOTO_STORE_MYPACK is the destination of the hub CentralUnclaimedPack tile
(tile type 0x1c); "Nothing in the chain issues a request, and no request could
exist" (pack-opening :888-890). Whether that HUB tile appears is gated by the
unopened-pack count in the hub model (model+0x20950) — i.e. the count DOES control
the hub unclaimed-pack tile, but the operator reached the store via Browse Packs
/ the store screen, whose category resolution is the movie-driven CATEGORY_ID path
(§4), not GOTO_STORE_MYPACK. GOTO_STORE_MYPACK is a UI navigation command,
not a server-state-gated store-category selector.
6. Candidate count/flag fields — verdict per field
unopenedPacks.recoveredPacks: correct at 0 when empty; controls the hub badge / CentralUnclaimedPack tile, not the store's category resolver. Not a viable gate for the store My-Packs entry.- No other server field feeds
screen+0x290(RE §4: only ctor-0 and movieCATEGORY_ID).
7. EA-capture evidence
No EA-origin purchasegroup/credits capture for a zero-unopened-packs account
exists in the repo (all captures are OpenFUT-generated). EA count semantics for empty
My Packs remain UNKNOWN.
8. Where My Packs selection occurs (OBSERVED)
Before purchasegroup parsing decides content, the Scaleform movie decides
which category to resolve and writes it to screen+0x290 (§4). The server's role is
limited to which groups EXIST in purchase[]. Therefore the sentinel is compensating
for a movie-side decision to resolve My Packs; it is not fixing an incorrect
server count (the count is already correct).
9. Candidate F verdict — F3 (CONTRADICTED) (with an F4 residue)
My Packs selection is not controlled by server-sent unopened-pack state:
- OBSERVED: the server count is correctly zero/absent when empty, yet the store still resolved My Packs (baseline dialog, Exp-B crash). A correct zero signal did not stop it.
- OBSERVED (RE):
screen+0x290(the resolved category) comes from the movie'sCATEGORY_ID, with no server-field input; default is 0. Residue (F4): the movie's internal logic for why it asks for My Packs on store open is in packed Scaleform and is not statically readable — but no server lever into it has been found. Conclusion: there is no server-controlled count/flag that makes FIFA skip resolving My Packs; the server can only ensure themypacksgroup exists. The "clean count-gated fix" is therefore not achievable server-side.
10. Proposed next experiment
Because F is contradicted, a count experiment is NOT recommended (the count is already correct and does not gate the store). No single-variable server signal will make FIFA enter Browse Packs instead of My Packs. The realistic next step is the previously deferred explicit active-placeholder selection test: with the C′ active non-openable placeholder in place (sentinel 65534 at baseline otherwise), have the operator explicitly focus/open the empty My-Packs tile and observe (server-safe per §16.1 — opening 65534 is a no-op — but UX/navigation behavior unknown). That characterizes the best available server-side option (C′) before any adoption.
- Would it change profile state? No (
unopenedPackIds=[]). - Would it change purchasegroup/sentinel behavior? Only
state:"active"(as C′), reverted after. - Code change? Yes (same one-line C′ patch). Restart? Yes. (Not authorized here.) If explicit selection proves unsafe/ugly, the remaining options are all client-side / out-of-scope (the decision is in the Scaleform movie), or accepting C′ with its documented UX artifacts.
Candidate F does NOT provide the hoped-for clean fix. The active non-openable placeholder (C′) remains the best server-side representation; its empty-tile and Browse-Packs navigation artifacts are movie-driven and not server-fixable.
Explicit Active-Placeholder Selection Test — FINAL backend-side result (2026-08-13)
With the C′ active placeholder in place (unopenedPackIds=[], 65534 active/mypacks/
∉PACK_CATALOG), the operator explicitly opened the empty My-Packs tile once. Full
record in STORE_TILE_6C.md §17.
- Outcome: S1 — pure client-side rejection. Dialog "This pack is no longer available" → back to My Packs → Hub; no crash, navigation stays usable.
- No server request on selection (no
/store/transaction, no/purchased/items, no 65534 reference); the verdict is client-side. (OBSERVED) - Zero economy/profile mutation: coins/items/nextItemId/
unopenedPackIds/last_packall unchanged; profile byte-identical (39bb3e83…); 65534 not persisted. (OBSERVED)
Active-placeholder verdict: MARGINALLY ACCEPTABLE — crash-safe + economy-safe + navigable, but with user-visible defects (empty fake tile; "no longer available" on explicit click; Browse-Packs nav gate). It is a strict improvement over the current inactive-sentinel baseline (which errors on store OPEN and bounces to Hub).
Backend-side question is now fully answered. The complete zero-unopened-packs ladder:
no mypacks group -> CardsDLL null-deref CRASH (unsafe)
inactive placeholder -> "pack not available" on store open -> Hub (baseline)
active placeholder -> store loads; empty tile; "no longer available" only on
explicit click; recoverable; economy-safe (best backend option)
real active owned pack -> fully correct UI (but grants a real openable pack — economy risk)
Permanent-fix recommendation: P2. The active non-openable placeholder is the best safe backend-only option, but a fully clean zero-pack experience is not achievable server-side (Candidate F CONTRADICTED — the My-Packs resolution is Scaleform/movie-driven). Recommend: adopt the active placeholder as an optional backend compatibility mode (safe, strictly better than baseline) AND pursue a client-side fix (hide the fake tile / stop the forced My-Packs resolution) for the fully clean result. Not implemented. Do NOT grant a real pack.
Client resolver-guard experiment (2026-08-13) — RESULT F3 (crash, confounded)
A client-side autopatch.py memory guard (CardsDLL 0x180014858 JNZ→JG, routing
category <0 to list-all/Browse) was tested against the exact no-sentinel server condition
(sentinel 65534 suppressed; GET /store/purchasegroup ids [1,5,6,7], no mypacks group).
The client crashed at the identical resolver site 0x180014882 ([NULL+0x48]), because
it presented a positive My-Packs ordinal (crash is in the >0 resolve branch), not the
-1 the guard diverts. Confound: FIFA was not relaunched after the backend flip, so it
reused stale (sentinel-present) tab state. So the negative-only guard is insufficient for a
positive stale/invalid ordinal, and the fresh-client case is not yet decided (needs a
clean re-test: fresh launch with backend already no-sentinel). Backend P2 sentinel was
restored immediately (mandatory rollback). Full record + candidate stronger guard:
docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md PART III.
Fresh-process no-sentinel retest (2026-08-13) — RESULT R1 (SUCCESS)
Re-ran the above cleanly: backend entered no-sentinel mode while FIFA was closed, then a
fresh FIFA (pid 553220, new autopatch 552999, guard 85 ff 7f 0f enforced) launched and
opened the Store. The genuine no-sentinel /store/purchasegroup (ids [1,5,6,7], no 65534/
mypacks) is byte-identical to the F3 capture, so the only changed variable is client
process lifetime. Outcome: no crash, no dialog, Store opens on Browse Packs, packs
navigable (cosmetics only: no tabs / no cover art / "0 items" — pre-existing). A fresh
client publishes category -1 for the absent group, which JNZ→JG routes to Browse/list-all
with no NULL deref. This confirms F3 was stale-positive-ordinal contamination, and proves
Strategy A (resolver guard) on the tested build. Backend P2 sentinel restored immediately
(f416e71e…, state=active) and remains production default. Full record:
docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md PART IV.