6746c75302
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.
383 lines
24 KiB
Markdown
383 lines
24 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.
|
||
|
||
---
|
||
|
||
# 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:**
|
||
```c
|
||
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 == 0` → `FUN_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).
|