Investigation + design only (no client/backend/binary changes, no live Store experiment). Reconfirmed CardsDLL_Win64_retail.dll (4706a881.., unpacked) against a freshly rebuilt Ghidra project on .105; FIFA17.exe (29c31cef..) is Denuvo-packed so the Scaleform decision is unreadable. PART II added to docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md: - Native category path traced: FUN_18007dab0 (store render, RVA 0x7dab0) reads screen+0x290; My Packs funnels through FUN_1800147f0 (0x147f0) -> FUN_180014420 (0x14420, NULL on ordinal miss) -> crash MOV [RDX+0x8] at 0x14882 ([NULL+0x48]), matching the Exp-B minidump. Tab->ordinal map FUN_180014580 (0=mypacks..5=special); category 0 = list-all (Browse). - Unopened-pack count is a data-manager singleton (vtbl[0x4d8] get / [0x4e0] set), reachable from the store resolver. - Vehicle: existing openfut-hook -> version.dll proxy (already deployed); reuse ssl_patch signature-scan + connect_hook inline detour. No new loader. - Preferred strategy A: entry-hook FUN_18007dab0; when the requested category is My Packs and unopened count==0, force screen+0x290=0 (Browse). Removes crash + fake 65534 tile + dialog + nav gate; count>0 untouched. - Ranked B (resolver NULL fallback, higher risk) and C (null-guard, crash-only). - Build guard: module gate + SHA/PE + signature scan; unknown build -> no patch, backend sentinel remains fallback. - First experiment design (needs a later, separately-authorized backend empty-no-sentinel test mode) + client rollback (config flag / dll swap). - Keep backend 65534 sentinel deployed until strategy A is verified. Describes the FIFA 17 client/data model only; not OpenFUT Core assumptions.
24 KiB
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 frompurchase[]; amypacksgroup exists iff some pack hasdisplayGroup.value=="mypacks".group+0x104=(value=="mypacks"), tiles ingroup+0x40, ordinal ingroup+0x00(1-based creation order).FUN_18007dab0→FUN_1800147f0(model, screen+0x290, …)— renders/resolves a category.screen+0x290==0lists group tiles (FUN_180014610); otherwiseFUN_180014420exact-matches the ordinal and returns NULL on a miss, after whichFUN_1800147f0dereferences[RAX+0x48]with no null guard → crash at0x180014882(ACCESS_VIOLATIONread of0x48, minidump-confirmed).screen+0x290is written in exactly two CardsDLL sites: ctorFUN_18007d1a0writes0;FUN_18007e7f0case0x7551copies the Flash movie message fieldCATEGORY_IDverbatim into it. So the category is chosen by the Scaleform movie.FUN_18007e5e0binds the six store tabs (FUN_180014580:mypacks, points, bronze, silver, gold, special) toPANEL_ID= matching group ordinal, or hides the panel.- Unopened-pack count signals (server, already correct at 0 when empty):
userInfo.unopenedPacks.recoveredPacksand/user/credits .unopenedPacks. The hubCentralUnclaimedPacktile (destinationGOTO_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 theCentralUnclaimedPacktile). - 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_IDis 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
mypacksis never resolved when absent. - Evidence:
screen+0x290 ← CATEGORY_ID(FUN_18007e7f0case0x7551); 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_180014420returns 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
mypacksgroup 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_IDselection 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
mypacksgroup 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.
PART II — Native client-fix design (RE-backed, 2026-08-13)
Investigation-and-design phase (no client binary/movie changed, no backend changed,
no new live Store experiment). CardsDLL was re-analysed in Ghidra on .105; the
in-repo decompiled addresses were reconfirmed against a freshly-built project. Every
claim below is labelled ESTABLISHED (read from this build's binary / crash dump),
PROPOSED (design, not yet implemented), or UNKNOWN.
6. Binary + environment verification (ESTABLISHED)
Hashes re-verified on .105 (/mnt/games/FIFA 17/) — identical to the recorded RE:
CardsDLL_Win64_retail.dllSHA-2564706a881ae1fc7b5769fd810b25a868d29d2b16a8e65a7513436327ef645573c, size 3179952, PETimeDateStamp1497050156 (2017-06-09T23:15:56Z),SizeOfImage0x31d000, image base0x180000000(RE-space). Unpacked → statically analysable.FIFA17.exeSHA-25629c31cef12b0c3c2a7305220617c7b4fa139ab76b8c857851bdbe88987962899, size 224639408, Denuvo-packed → the Scaleform/StoreFront ActionScript that decides to emitCATEGORY_IDis NOT statically readable. This is why a pure-Scaleform edit (old "Rank 1") is not the practical vehicle; the fix is taken at the readable native boundary in CardsDLL instead.- Ghidra project rebuilt at
.105:/tmp/ghidra_fut/cardsdll(headless import+analysis succeeded). Tooling:fifa17-recon/tools/ghidra_env.pyrun under~/.venv(PYTHONPATH=/opt/ghidra/Ghidra/Features/PyGhidra/pypkg/src:/usr/lib/python3.14/site-packages;jpype1reinstalled offline from pip cache). RVAs below = static VA −0x180000000.
7. Category-selection path (ESTABLISHED — decompiled this build)
Store message dispatch FUN_18007d880 (RVA 0x7d880) routes Flash message ids:
0x753f → FUN_18007dab0 (render), 0x278a → FUN_18007df60 (publish category ids),
and the input handler FUN_18007e7f0 (RVA 0x7e7f0) case 0x7551 copies the
movie field CATEGORY_ID verbatim into screen+0x290 (the only non-ctor writer;
ctor FUN_18007d1a0 writes 0).
Store render FUN_18007dab0 (RVA 0x7dab0), decompiled verbatim, is the decision
point:
iVar1 = *(int *)(param_1 + 0x290); // requested CATEGORY_ID (screen+0x290)
iVar6 = FUN_180014580(store, 1); // the *points* category id (see tab map)
if (iVar1 == iVar6) { // requested category is POINTS (real-money)
if (region_check() == 0) { post "REGION_MISMATCH"; return; }
if (FUN_180014de0(store) != 0) return; // points group present → handled
FUN_180014b60(store, dp); // else points render
} else {
FUN_1800147f0(store, iVar1, dp, 0, 0); // EVERY other category, incl. My Packs
}
param_1(RCX) = the store-screen object;+0x290is the requested category.- Tab→id map
FUN_180014580(store, n)(RVA0x14580):0=mypacks, 1=points, 2=bronze, 3=silver, 4=gold, 5=special. Each returns the group's 1-based ordinal (via caption compareFUN_180014380) or-1if that group is absent. So category ids are DYNAMIC ordinals, not fixed constants. The tab publisherFUN_18007df60pushesMYPACK_/BRONZE_/…_CATEGORY_IDto the movie from these lookups; the movie echoes one back asCATEGORY_ID. - The points tab is the only one special-cased (commerce/region gate). My Packs
is NOT special-cased — it falls into the
elseand is resolved byFUN_1800147f0.
Resolver FUN_1800147f0 (RVA 0x147f0) — the crash (ESTABLISHED, instruction
level):
0x14856: 85 ff TEST EDI,EDI ; EDI = category ordinal (param_2)
0x14858: 75 0f JNZ 0x14869 ; ==0 → list-all (Browse), else resolve
0x1485a: … CALL 0x14610 ; FUN_180014610 list ALL group tiles
0x14867: eb 29 JMP 0x14892
0x14869: 8b d7 MOV EDX,EDI
0x1486b: e8 … CALL 0x14420 ; FUN_180014420(store, ordinal) → RAX (group|NULL)
0x14870: 48 8d 50 40 LEA RDX,[RAX + 0x40] ; RDX = group+0x40 (=0x40 when RAX=NULL)
0x14878: 48 3b c2 CMP RAX,RDX
0x1487b: 74 15 JZ 0x14892
0x14882: 4c 8b 42 08 MOV R8,[RDX + 0x8] ; <-- FAULT: read [0x40+0x8]=0x48 when NULL
0x14886: 48 8b 12 MOV RDX,[RDX] ; [0x40]
FUN_180014420 (RVA 0x14420) exact-matches group+0x00 (ordinal), stride 0x108,
returns NULL on a miss, with no guard in the caller → faulting read of VA 0x48
at 0x180014882. This is byte-for-byte the Experiment-B minidump
(0xC0000005 READ 0x48 at CardsDLL+0x14882).
param_2 == 0→FUN_180014610lists all group tiles = the safe "Browse Packs" view.param_2 == existing ordinal→ resolves.param_2 == a non-existent ordinal(e.g.-1, whichMYPACK_CATEGORY_IDbecomes when the group is absent) → NULL → crash.
Why it crashes with zero packs (ESTABLISHED): with unopenedPackIds==[] and no
sentinel, no mypacks group exists, so FUN_180014580(store,0) = -1,
MYPACK_CATEGORY_ID = -1, the movie still selects My Packs and echoes CATEGORY_ID = -1, and FUN_1800147f0(store, -1, …) → FUN_180014420(-1)=NULL → crash. The active
sentinel (65534) works only because it makes a real mypacks ordinal exist to resolve.
8. Zero-pack state client-side (ESTABLISHED)
The client already holds the correct unopened-pack count in a data-manager singleton (the same one the store resolver uses):
- Obtain:
seed = FUN_1800d7170()thenFUN_180009c80(&p, seed)→p(release withp->vtbl[0x08](p)). This exact accessor already runs insideFUN_180014420andFUN_1800147f0, so any store-category hook can reach it. - Read count:
p->vtbl[0x4d8](p)→ int. Write:p->vtbl[0x4e0](p, n). Confirmed inFUN_180019780, which reads slot0x4d8, adds the number of set booleans in a pack response, and writes slot0x4e0(it also fetchesFutGetPurchasedItems). - Representation: plain
int; 0 = no unopened packs,>0= count. Lifetime: the singleton persists for the session; updated on pack acquire/open. - No dedicated "hasUnopenedPacks" boolean helper was found;
count != 0is the predicate. (The hubCentralUnclaimedPacktile is gated by this same count viamodel+0x20950, written byFUN_18010cdc0/FUN_18011e120— the hub mirror, not the store gate.)
9. Implementation vehicle (ESTABLISHED — reuse, do not build a new loader)
OpenFUT already ships a client hook framework: openfut-launcher/openfut-hook
(crate-type=["cdylib"]) builds version.dll, a proxy DLL placed in the game dir
(/mnt/games/FIFA 17/version.dll, present & active; log ~/.wine/drive_c/openfut_hook.log).
- Load path: Wine/Windows loads
version.dllfrom the app dir at process start →DllMain(DLL_PROCESS_ATTACH)→install_hooks(). - Existing hooks (
lib.rs):getaddrinfo(IAT viaiat::resolve),connect(inline detour),WSAConnect,WSAIoctl/ConnectEx, origin_spy registry/mutex, crypt32CertVerifyCertificateChainPolicy, and in-memory byte-patching of the loaded (packed) main exe + EAWebKit (ssl_patch:GetModuleHandleA→ scan for a unique prologue →VirtualProtect+copy_nonoverlapping). - Inline-hook primitive (
connect_hook):write_hook(target, dest)lays a 14-byteFF 25 00000000 <abs64>JMP;restore_originalrestores saved bytes (unhook → call real → rehook, avoiding trampoline relocation). - Config:
openfut.cfgbeside the DLL (host/ports today; astore_mypacks_fixflag would be added there). - Suitability for the Store fix: direct. The DLL is in-process with full access
to the loaded
CardsDLL_Win64_retail.dll; the store fix is a NEW module (store_hook.rs) installed frominstall_hooks, reusing thessl_patchsignature-scan and theconnect_hookinline-detour patterns. No new loader, no ASI, no separate injector.
10. Three strategies re-evaluated against the RE (Task 4)
A. Category-selection redirect — PREFERRED (best UX, native, targeted)
Hook FUN_18007dab0 (RVA 0x7dab0) at entry; before the original runs, redirect a
zero-pack My-Packs request to Browse Packs:
cat = *(int*)(store + 0x290)
mypacks_id = FUN_180014580(store, 0) // -1 when the group is absent
if (cat == mypacks_id) { // movie asked for My Packs (incl. cat==-1==id)
if (unopened_count() == 0) // singleton vtbl[0x4d8]
*(int*)(store + 0x290) = 0; // 0 = FUN_180014610 list-all = Browse Packs
}
// then call the original FUN_18007dab0(store)
- Uses the real count? Yes (singleton
vtbl[0x4d8]). Removes the fake 65534 tile? Yes (server can omit the group). Removes the click-dialog? Yes (no placeholder to click). Removes the Browse→My-Packs nav gate? Yes (store lands on Browse, not an empty My-Packs). Preserves count>0? Yes (cat==mypacks_idwith count>0 is left untouched → normal My Packs). Affects other categories? No (cat!=mypacks_idpath is unmodified; points/bronze/… unchanged). - Prevents the crash as a side effect (My Packs is never resolved when its group is absent). This is the old "Rank 1" INTENT, implemented at the readable native boundary instead of in packed Scaleform.
B. Resolver fallback — acceptable safety net, less targeted
In FUN_1800147f0 (or right after the CALL 0x14420 at RVA 0x1486b): if the
resolved group is NULL, fall back to list-all (param_2=0) instead of dereferencing.
- Prevents crash? Yes. Fixes default nav / removes fake tile? Partially — the
movie still believes it is in My Packs, so the view may be an empty/odd My-Packs
rather than a clean Browse. Leaves other lookups unchanged? It changes miss-handling
for ALL categories — a generic NULL fallback that could mask a genuine
missing-category protocol bug. Higher risk than A for that reason; keep as a
belt-and-braces guard, not the primary UX fix. The resolver does NOT know why
mypacksis missing, which is exactly the concern the task flags.
C. Null-guard only — weakest (crash-only)
Insert TEST RAX,RAX; JZ 0x14892 immediately after CALL 0x14420 (RVA 0x1486b),
before LEA RDX,[RAX+0x40]. Needs a trampoline (no inline slack).
- Converts the crash into whatever an empty tile-vector renders (unverified; likely a blank/empty category). Does not remove the fake tile or fix the default category; the sentinel would still be needed for acceptable UX. Verified as expected-weakest.
11. Concrete hook target for strategy A (Task 6, PROPOSED)
module: CardsDLL_Win64_retail.dll (GetModuleHandleA)
function: FUN_18007dab0 (store render / message 0x753f)
RVA: 0x7dab0 (static VA 0x18007dab0)
calling conv: Microsoft x64 fastcall; single arg store-screen ptr in RCX
screen offset: store+0x290 = requested CATEGORY_ID (int)
helpers to call: FUN_180014580 (RVA 0x14580) tab→ordinal, arg0=RCX store, arg1=EDX index(0=mypacks)
count singleton: FUN_1800d7170 (0xd7370-seed) + FUN_180009c80 (0x9c80), read vtbl[0x4d8]
redirect target: set store+0x290 = 0 (FUN_180014610 list-all → Browse Packs)
original behavior: zero packs → resolves absent mypacks ordinal → FUN_180014420 NULL → crash at 0x14882
desired behavior: zero packs + mypacks requested → store+0x290 forced to 0 → Browse Packs; no crash/dialog/tile
Hook mechanics (reuse connect_hook): lay a 14-byte FF 25 JMP at base+0x7dab0 to a
Rust hooked_store_render(store); inside: apply the redirect, unhook, call real
FUN_18007dab0(store), rehook, return its value. Intercepting only the entry means the
minimum interception is the 14 JMP bytes; the first instructions of FUN_18007dab0
(MOV RAX,RSP; MOV [RAX+8],RCX; PUSH …) are a standard prologue safe to save/restore.
Alt insertion point (earlier): FUN_18007e7f0 case 0x7551, where CATEGORY_ID is
written to screen+0x290 — redirect there instead of at render. Entry-hook of
FUN_18007dab0 is preferred (single, well-typed arg; runs once per store render).
Thread/context: the store screen runs on the client's UI/update thread; the hook reads
one int and (rarely) writes one int on the same object the callee immediately reads —
no new synchronization needed. Called for categories other than My Packs? The FUNCTION
is, but the redirect body only fires when cat==mypacks_id, so other tabs are
untouched.
12. Version / build safety (Task 7, PROPOSED)
FIFA17-specific compat code MUST validate the client before hooking, and MUST no-op on
any other build (the same version.dll is also used for FIFA23):
- Module gate: only proceed if
GetModuleHandleA("CardsDLL_Win64_retail.dll")resolves (FIFA23 has no such module → auto-skip). - Build gate (both, belt-and-braces):
- Exact hash/PE gate: on-disk SHA-256 ==
4706a881…, or PESizeOfImage==0x31d000&&TimeDateStamp==1497050156(cheap in-memory check). - Signature scan + validation: locate
FUN_18007dab0by a unique prologue/byte window rather than trusting the RVA, and assert the known bytes at the branch (85 ff 75 0fregion) and at the resolverCALL 0x14420site match before installing. Recommend both: hash to reject the wrong game fast, signature to confirm the exact patch site.
- Exact hash/PE gate: on-disk SHA-256 ==
- Failure behavior: any check fails (unknown/updated build) → do NOT patch, log, and leave the backend P2 active-sentinel (65534) as the fallback. Never patch or crash an unrecognised build.
13. First controlled client experiment (Task 8, PROPOSED — not executed here)
Goal: prove a patched client sends zero-pack Store entry to Browse Packs with no active placeholder.
- Build
openfut-hookwith strategy-Astore_hook, gated behindopenfut.cfgstore_mypacks_fix=1(opt-in; default off preserves today's behavior). - Test profile:
unopenedPackIds == []. - Sequence (each variable changed alone; operator drives FIFA; read-only capture):
- Deploy patched
version.dll; confirmopenfut_hook.logshows the store hook installed + build gate PASSED. - Backend test mode (LATER, separately authorized — NOT in this task): switch the
backend to empty-no-sentinel (the Exp-B config that crashed the UNPATCHED client)
so the patched client must handle a genuinely-absent
mypacksgroup. - Operator opens Store. Predicted (patched + zero packs + no sentinel): Store
opens, defaults to Browse Packs, no
mypacksresolve, no crash, no dialog, no fake tile. - Set
unopenedPackIds=[70]; reopen. Predicted: My Packs works normally (hook body skipped because count>0). - Revert backend to the active sentinel.
- Deploy patched
- Backend change eventually required for this experiment: YES — a controlled empty-no-sentinel test mode to force the absent group. It is NOT performed in this phase and MUST be separately authorized (same experiment discipline: patch the container copy, capture, revert, restart; never synthesize a client request).
- Client rollback: flip
store_mypacks_fix=0(hook not installed) or restore the originalversion.dll; the game reverts to depending on the backend sentinel. No FIFA binaries/movies/config are modified on disk — the hook is in-memory only, so rollback is a file/flag swap.
14. Interaction with the backend 65534 fallback (ESTABLISHED + PROPOSED)
- Keep the backend sentinel deployed until strategy A is implemented AND verified. It remains the required behavior for unpatched retail clients and for any client whose build gate fails.
- Once strategy A is verified, the server MAY, for patched clients only, omit the
mypacksgroup when empty (the safe representation the client will then handle) — but only behind explicit detection/opt-in; do NOT drop the sentinel globally, since unpatched clients still crash without it.
15. ESTABLISHED / PROPOSED / UNKNOWN summary
- ESTABLISHED: binary hashes/build; the full native category path and addresses
(
FUN_18007d880/18007dab0/18007e7f0/1800147f0/180014420/180014580/180014610); the instruction-level crash (0x14882,[NULL+0x48]); tab→ordinal map; that My Packs is not special-cased and funnels throughFUN_1800147f0; the unopened-count singleton and itsvtbl[0x4d8]/[0x4e0]accessors, reachable from store code; theopenfut-hook/version.dllvehicle and its hook/patch primitives. - PROPOSED (not implemented): the strategy-A entry hook and its redirect logic; the build-guard scheme; the opt-in config flag; the first experiment and its backend test-mode requirement; the per-patched-client server relaxation.
- UNKNOWN: exactly why the packed Scaleform movie selects My Packs on store open
(Denuvo-packed, unread) — not needed for strategy A, which intercepts the native
result; the precise rendered appearance of
category==0list-all in this empty configuration (to be observed in the experiment); whether any non-store path also drivesscreen+0x290to a My-Packs ordinal (none found;FUN_18007e7f0case0x7551and the ctor are the only writers).