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).
This commit is contained in:
funman300
2026-08-13 01:08:47 +00:00
parent f42279f869
commit e8ee6c34e7
7 changed files with 2222 additions and 0 deletions
@@ -0,0 +1,115 @@
# FIFA 17 — Clean Empty-My-Packs client fix (DESIGN / RESEARCH ONLY)
Status: **design only — no client binary/movie changes made.** This is the client-side
follow-up to bug 6c. The backend already ships a compatibility workaround (P2, active
non-openable sentinel 65534; see `docs/evidence/STORE_TILE_6C.md` §17 and
`FIFA17_EMPTY_MYPACKS_CLIENT_CONTRACT.md`). This plan describes what a *client-side*
fix would need to change so the backend shim can eventually become unnecessary for
patched clients.
Do NOT patch the executable, DLLs, or Scaleform movies in this task.
## 1. Established client-side evidence
Binary: `CardsDLL_Win64_retail.dll`
SHA-256 `4706a881ae1fc7b5769fd810b25a868d29d2b16a8e65a7513436327ef645573c`
(dump load base `0x00006FFFFC120000`; RE-space base `0x180000000`). `FIFA17.exe`
(`29c31cef…`) is Denuvo-packed (decrypts only in live memory).
Store category pipeline (all decompiled; see `docs/plan-2026-08-05-store-subsystem.md`):
- `FUN_1800150d0` — builds display groups from `purchase[]`; a `mypacks` group exists
iff some pack has `displayGroup.value=="mypacks"`. `group+0x104=(value=="mypacks")`,
tiles in `group+0x40`, ordinal in `group+0x00` (1-based creation order).
- `FUN_18007dab0``FUN_1800147f0(model, screen+0x290, …)` — renders/resolves a
category. `screen+0x290==0` lists group tiles (`FUN_180014610`); otherwise
`FUN_180014420` exact-matches the ordinal and **returns NULL on a miss**, after
which `FUN_1800147f0` dereferences `[RAX+0x48]` with **no null guard**
**crash at `0x180014882`** (`ACCESS_VIOLATION` read of `0x48`, minidump-confirmed).
- `screen+0x290` is written in exactly two CardsDLL sites: ctor `FUN_18007d1a0`
writes `0`; **`FUN_18007e7f0` case `0x7551` copies the Flash movie message field
`CATEGORY_ID` verbatim** into it. So the category is chosen by the Scaleform movie.
- `FUN_18007e5e0` binds the six store tabs (`FUN_180014580`: `mypacks, points, bronze,
silver, gold, special`) to `PANEL_ID` = matching group ordinal, or hides the panel.
- Unopened-pack count signals (server, already correct at 0 when empty):
`userInfo.unopenedPacks.recoveredPacks` and `/user/credits .unopenedPacks`. The hub
`CentralUnclaimedPack` tile (destination `GOTO_STORE_MYPACK`) is gated by this count
in the hub model (`model+0x20950`). **Candidate F (a server count gating the STORE's
My-Packs resolution) was CONTRADICTED**: the count is correct at 0 yet the store
still resolves My Packs, because the decision is movie-side.
## 2. Desired clean client behavior
```
unopened-pack count == 0:
Store defaults to Browse Packs (e.g. a real category such as bronze/gold)
My Packs is NOT selected/resolved
no synthetic placeholder tile is required from the server
unopened-pack count > 0:
existing My Packs behavior unchanged
```
## 3. Candidate insertion points (ranked)
Ranking favors fixing the UX (not merely preventing the crash) and the smallest,
lowest-risk change that achieves it.
### Rank 1 (preferred, best UX) — Scaleform / category-selection layer
Prevent the movie from emitting `CATEGORY_ID == mypacks` (and from defaulting the
store into My Packs) when the unopened-pack count is 0; default to Browse Packs
instead.
- **Where:** the FUT Store Scaleform movie / ActionScript (`StoreFront`,
`CATEGORY_ID`/`ACTION_GET_PACKLIST`, `GOTO_STORE_MYPACK`), which the packed exe hosts
and which reads the hub model (it already knows the count for the
`CentralUnclaimedPack` tile).
- **Behavior changed:** the store's initial/selected category when empty.
- **Scope:** movie asset edit (client-side), no native-code patch.
- **Risk:** medium — Scaleform RE/editing is fiddly; must find where the default
`CATEGORY_ID` is chosen and gate it on the count without breaking the count>0 path.
- **Compatibility:** per-client asset change; does not touch protocol or other clients.
- **Fixes UX or just crash?** **UX** — no fake tile, correct default; the crash also
disappears because `mypacks` is never resolved when absent.
- **Evidence:** `screen+0x290 ← CATEGORY_ID` (`FUN_18007e7f0` case `0x7551`); count
already available client-side (hub model / `unopenedPacks`).
### Rank 2 — Native Store resolver fallback (CardsDLL)
Make `FUN_1800147f0`/`FUN_180014420` fall back to a safe category (e.g. list-tiles
`N==0`, or the first existing group) when the requested ordinal misses, instead of
dereferencing NULL.
- **Behavior changed:** category-miss handling for ALL categories, not just mypacks.
- **Scope:** small, localized CardsDLL binary patch near `0x180014420`/`0x180014882`.
- **Risk:** medium — alters native store behavior globally; could mask other
legitimate misses; the movie may still believe it is in My Packs (empty/odd view).
- **Compatibility:** binary patch to the shipped DLL (client-side).
- **Fixes UX or just crash?** Crash + partial UX (no crash, but the empty-My-Packs
view may still be awkward).
- **Evidence:** the no-guard deref at `0x180014882`; `FUN_180014420` returns NULL on
miss.
### Rank 3 (cheapest, crash-only) — CardsDLL null guard
Insert a null check before the `[RAX+0x48]` dereference in `FUN_1800147f0` (a single
`TEST/JZ` around the deref) so a NULL group is skipped/returned safely.
- **Behavior changed:** only the crash path.
- **Scope:** minimal (a few bytes) binary patch at `~0x180014882`.
- **Risk:** low — smallest change; but purely crash-prevention. With no `mypacks`
group the resulting empty view is unverified (could be a blank/empty-category state).
- **Compatibility:** binary patch (client-side).
- **Fixes UX or just crash?** Crash only.
- **Evidence:** minidump faulting instruction `CardsDLL+0x14882`, `[NULL+0x48]`.
## 4. Recommended long-term outcome
Rank 1 (Scaleform default-category gating) is the clean fix: with count 0 the store
opens on Browse Packs, no `mypacks` resolution, no fake tile — and the **backend
sentinel 65534 can be dropped for patched clients** (the server would simply omit the
`mypacks` group when empty, which is safe once the client no longer resolves it).
Rank 3 (null guard) is a cheap universal crash-safety net that could ship alongside.
Until a client-side fix exists, the backend P2 sentinel remains the required
compatibility behavior for unpatched retail clients.
## 5. Open questions / next research (no execution here)
- Locate the Store movie's default/initial `CATEGORY_ID` selection and confirm it can
read the unopened count (Rank 1 feasibility).
- Confirm, via a guarded-resolver experiment, what the empty-My-Packs view degrades to
if the `mypacks` group is simply absent + a null guard is present (Rank 2/3).
- Determine whether the Browse-Packs→My-Packs navigation gate (observed with the
active sentinel) also resolves under Rank 1.