e8ee6c34e7
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).
116 lines
6.6 KiB
Markdown
116 lines
6.6 KiB
Markdown
# 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.
|