Files
OpenFUT/docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md
T
funman300 6746c75302 docs(fifa17): RE-backed native client-fix design for empty My Packs
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.
2026-08-13 01:48:58 +00:00

24 KiB
Raw Blame History

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_18007dab0FUN_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 guardcrash 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].

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.

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.dll SHA-256 4706a881ae1fc7b5769fd810b25a868d29d2b16a8e65a7513436327ef645573c, size 3179952, PE TimeDateStamp 1497050156 (2017-06-09T23:15:56Z), SizeOfImage 0x31d000, image base 0x180000000 (RE-space). Unpacked → statically analysable.
  • FIFA17.exe SHA-256 29c31cef12b0c3c2a7305220617c7b4fa139ab76b8c857851bdbe88987962899, size 224639408, Denuvo-packed → the Scaleform/StoreFront ActionScript that decides to emit CATEGORY_ID is 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.py run under ~/.venv (PYTHONPATH=/opt/ghidra/Ghidra/Features/PyGhidra/pypkg/src:/usr/lib/python3.14/site-packages; jpype1 reinstalled 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; +0x290 is the requested category.
  • Tab→id map FUN_180014580(store, n) (RVA 0x14580): 0=mypacks, 1=points, 2=bronze, 3=silver, 4=gold, 5=special. Each returns the group's 1-based ordinal (via caption compare FUN_180014380) or -1 if that group is absent. So category ids are DYNAMIC ordinals, not fixed constants. The tab publisher FUN_18007df60 pushes MYPACK_/BRONZE_/…_CATEGORY_ID to the movie from these lookups; the movie echoes one back as CATEGORY_ID.
  • The points tab is the only one special-cased (commerce/region gate). My Packs is NOT special-cased — it falls into the else and is resolved by FUN_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 == 0FUN_180014610 lists all group tiles = the safe "Browse Packs" view. param_2 == existing ordinal → resolves. param_2 == a non-existent ordinal (e.g. -1, which MYPACK_CATEGORY_ID becomes 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() then FUN_180009c80(&p, seed)p (release with p->vtbl[0x08](p)). This exact accessor already runs inside FUN_180014420 and FUN_1800147f0, so any store-category hook can reach it.
  • Read count: p->vtbl[0x4d8](p) → int. Write: p->vtbl[0x4e0](p, n). Confirmed in FUN_180019780, which reads slot 0x4d8, adds the number of set booleans in a pack response, and writes slot 0x4e0 (it also fetches FutGetPurchasedItems).
  • 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 != 0 is the predicate. (The hub CentralUnclaimedPack tile is gated by this same count via model+0x20950, written by FUN_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.dll from the app dir at process start → DllMain(DLL_PROCESS_ATTACH)install_hooks().
  • Existing hooks (lib.rs): getaddrinfo (IAT via iat::resolve), connect (inline detour), WSAConnect, WSAIoctl/ConnectEx, origin_spy registry/mutex, crypt32 CertVerifyCertificateChainPolicy, 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-byte FF 25 00000000 <abs64> JMP; restore_original restores saved bytes (unhook → call real → rehook, avoiding trampoline relocation).
  • Config: openfut.cfg beside the DLL (host/ports today; a store_mypacks_fix flag 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 from install_hooks, reusing the ssl_patch signature-scan and the connect_hook inline-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_id with count>0 is left untouched → normal My Packs). Affects other categories? No (cat!=mypacks_id path 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 mypacks is 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):

  1. Module gate: only proceed if GetModuleHandleA("CardsDLL_Win64_retail.dll") resolves (FIFA23 has no such module → auto-skip).
  2. Build gate (both, belt-and-braces):
    • Exact hash/PE gate: on-disk SHA-256 == 4706a881…, or PE SizeOfImage==0x31d000 && TimeDateStamp==1497050156 (cheap in-memory check).
    • Signature scan + validation: locate FUN_18007dab0 by a unique prologue/byte window rather than trusting the RVA, and assert the known bytes at the branch (85 ff 75 0f region) and at the resolver CALL 0x14420 site match before installing. Recommend both: hash to reject the wrong game fast, signature to confirm the exact patch site.
  3. 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-hook with strategy-A store_hook, gated behind openfut.cfg store_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):
    1. Deploy patched version.dll; confirm openfut_hook.log shows the store hook installed + build gate PASSED.
    2. 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 mypacks group.
    3. Operator opens Store. Predicted (patched + zero packs + no sentinel): Store opens, defaults to Browse Packs, no mypacks resolve, no crash, no dialog, no fake tile.
    4. Set unopenedPackIds=[70]; reopen. Predicted: My Packs works normally (hook body skipped because count>0).
    5. Revert backend to the active sentinel.
  • 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 original version.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 mypacks group 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 through FUN_1800147f0; the unopened-count singleton and its vtbl[0x4d8]/[0x4e0] accessors, reachable from store code; the openfut-hook/version.dll vehicle 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==0 list-all in this empty configuration (to be observed in the experiment); whether any non-store path also drives screen+0x290 to a My-Packs ordinal (none found; FUN_18007e7f0 case 0x7551 and the ctor are the only writers).