feat: document and stage FIFA 17 SBC hook workflow
This commit is contained in:
@@ -9,4 +9,5 @@
|
||||
*.log
|
||||
__pycache__/
|
||||
captures/
|
||||
staging/
|
||||
tools/fifa17_profile.json
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
# FIFA 17 SBC client-hook implementation plan
|
||||
|
||||
## Outcome
|
||||
|
||||
Implement an opt-in, fail-closed hook that repairs the native response-to-deserializer
|
||||
dispatch for `GET /ut/game/fifa17/sbs/sets`. The hook must reuse the genuine response
|
||||
object and SAX reader from the real HTTP 200 transaction, run synchronously on the native
|
||||
transaction thread, and preserve the game's allocator, object ownership, callbacks, and
|
||||
index rebuilds.
|
||||
|
||||
This plan supersedes the intervention direction in `plan-2026-08-07-sbc-hook.md` and
|
||||
`sbc-hook-dll-spec.md` wherever those documents claim the client never issues `/sbs/sets`
|
||||
or recommend constructing a synthetic reader. The fresh 10:20:20 exchange proves the
|
||||
request is issued and receives populated JSON. The reconciliation report is authoritative.
|
||||
|
||||
## Proven anchors
|
||||
|
||||
All addresses are static VAs in `CardsDLL_Win64_retail.dll`, image base `0x180000000`.
|
||||
Runtime addresses are `CardsDLL base + (static VA - 0x180000000)`.
|
||||
|
||||
| Purpose | Address / identity |
|
||||
|---|---|
|
||||
| Category request constructor | `0x18017a7c0`, request vtable `0x18022e5c0`, tag `0x753c` |
|
||||
| `/sets` URI builder | `0x18017a980` |
|
||||
| Typed response factory | `0x18017aa10`, response vtable `0x18022e5b0` |
|
||||
| Typed category deserializer | `0x18017b2b0`, `rcx=response`, `rdx=genuine reader` |
|
||||
| Generic completion | `0x18016cca0`, exact-200 check at `0x18016cdd0` |
|
||||
| FUT root | `A = *0x1802e6398`, expected vtable `0x18021c2a0` |
|
||||
| SBC gate cache | `B=A+0x1f9d8`; ready byte `B+0x28` |
|
||||
| Category store | `M=*(A+0x20a68)`; count `WORD[M+0x50]` |
|
||||
| Renderer count read | `0x1800b5eda` |
|
||||
|
||||
Entering `0x18017b2b0` necessarily invokes the `A+0x20a68` lazy getter before JSON-key
|
||||
parsing. The fresh transaction left that pointer null, proving that the typed category
|
||||
deserializer was not entered.
|
||||
|
||||
## Architecture decision
|
||||
|
||||
Use the existing `openfut-hook` Rust `cdylib` and FIFA 17 feature boundary. Retain its
|
||||
deferred CardsDLL discovery, RVA calculation, guarded reads, default-off environment
|
||||
gates, and logging. Replace the stale Tier-1 idea of constructing a reader with this flow:
|
||||
|
||||
```text
|
||||
real /sbs/sets HTTP 200
|
||||
-> native generic completion and typed-response factory
|
||||
-> observe the real response object and real reader/body cursor
|
||||
-> at the proven skipped dispatch boundary, call the original typed method once
|
||||
-> native parser populates M and rebuilds its indices
|
||||
-> resume the native callback/completion chain
|
||||
-> validate M; use native gate state if available
|
||||
-> only if necessary, arm B+0x28 while B+0x08 remains zero
|
||||
```
|
||||
|
||||
Do not intercept at the socket layer, fabricate a SAX reader, retain response/reader
|
||||
pointers beyond their synchronous lifetime, hand-build EASTL category/set records, or
|
||||
write `B+0x08`/`B+0x20`.
|
||||
|
||||
## State and feature gates
|
||||
|
||||
Use independent flags; no stronger stage should be implied by a weaker one:
|
||||
|
||||
- `OPENFUT_SBC_HOOK=1`: resolve and fingerprint only.
|
||||
- `OPENFUT_SBC_TRACE=1`: install passive probes and structured logging.
|
||||
- `OPENFUT_SBC_DISPATCH=1`: enable the one-shot native dispatch repair.
|
||||
- `OPENFUT_SBC_COMMIT=1`: permit gate/refresh action after validated parse success.
|
||||
- Keep `OPENFUT_SBC_ARM_ONLY=1` solely as a separate negative-control experiment.
|
||||
|
||||
Represent runtime progress with an atomic state machine:
|
||||
|
||||
```text
|
||||
Disabled -> Resolved -> Intercepted -> Parsed -> Validated -> Committed
|
||||
\-> Failed
|
||||
```
|
||||
|
||||
Add a recursion-depth guard and a transaction one-shot keyed by request/response identity.
|
||||
Any fingerprint, pointer, status, class, thread, reader, or postcondition mismatch moves to
|
||||
`Failed` and resumes native execution without a write.
|
||||
|
||||
## Milestones
|
||||
|
||||
### M0 — reconcile and freeze the baseline
|
||||
|
||||
1. Mark the reconciliation report as the address/path authority.
|
||||
2. Record SHA-256, PE timestamp, `SizeOfImage`, and selected section hashes for the shipped
|
||||
CardsDLL, FIFA executable, built hook, and deployed proxy DLL.
|
||||
3. Preserve a known-good launcher and proxy DLL. Do not overwrite a game-directory DLL
|
||||
without an exact backup and hashes.
|
||||
4. Capture a baseline: FUT hub succeeds, `/sbs/sets` returns 200, SBC shows the modal,
|
||||
`M==0`, and the category deserializer is not observed.
|
||||
|
||||
Exit: the baseline is repeatable and its artifacts identify one binary build exactly.
|
||||
|
||||
### M1 — stabilize DLL loading
|
||||
|
||||
The existing `version.dll` injection has one historical successful log, but the current
|
||||
FIFA 17 launcher disables it after later crashes. Resolve this before SBC detours:
|
||||
|
||||
1. Port or implement the complete VERSION proxy export surface and forward every export.
|
||||
2. Build only `--features fifa17` for `x86_64-pc-windows-gnu` into a staging directory.
|
||||
3. Inspect PE architecture, exports, and imports with the MinGW binutils.
|
||||
4. Add a FIFA-17-specific launch path using the existing prefix/UMU configuration and
|
||||
explicit `WINEDLLOVERRIDES=version=n,b`.
|
||||
5. Run three cold launches with every SBC mutation/trace flag disabled.
|
||||
|
||||
Exit: all three launches reach the FUT hub, VERSION calls forward correctly, and disabling
|
||||
the override restores the pre-hook baseline.
|
||||
|
||||
### M2 — strengthen runtime resolution
|
||||
|
||||
Before any detour or byte write, validate:
|
||||
|
||||
- exact CardsDLL identity (`SizeOfImage`, PE metadata, and multiple section/function hashes);
|
||||
- FNV control bytes at `0x180180d00`;
|
||||
- expected bytes at every proposed patch site;
|
||||
- `A` and its expected vtable;
|
||||
- `B` and its expected vtable;
|
||||
- readable `M` slot and sane cache fields; and
|
||||
- that runtime VAs lie inside the expected CardsDLL sections.
|
||||
|
||||
Use the external read-only `futmem`/probe tooling as an independent oracle. Never cache an
|
||||
ASLR slide across launches.
|
||||
|
||||
Exit: resolve-only mode passes on two launches with different slides and aborts cleanly on
|
||||
a deliberately mismatched fingerprint fixture.
|
||||
|
||||
### M3 — passive transaction tracing
|
||||
|
||||
Instrument, without changing return values or state:
|
||||
|
||||
1. generic completion `0x18016cca0`;
|
||||
2. typed response factory `0x18017aa10`;
|
||||
3. typed category deserializer `0x18017b2b0`; and
|
||||
4. once found, the common body/SAX virtual-dispatch callsite.
|
||||
|
||||
Log a monotonic timestamp, session/build ID, thread ID, recursion depth, status, request
|
||||
pointer/vtable, response pointer/vtable, reader/body pointer and vtable, and `M`/`B`
|
||||
before and after. Correlate a request ordinal with `/tmp/utas.log`; do not log SID/auth
|
||||
values or full response bodies.
|
||||
|
||||
Do not use the existing generic four-register probe wrapper for `0x18016cca0`. That routine
|
||||
has a fifth stack argument. Use a relocated trampoline or a narrowly verified assembly
|
||||
stub that preserves the full Win64 ABI: nonvolatile GPRs, XMM6-XMM15 if touched, 32-byte
|
||||
shadow space, 16-byte call alignment, and all stack arguments. The diagnostic
|
||||
unhook/call/rehook mechanism is also racy and is not acceptable for the final repair.
|
||||
|
||||
Exit: one fresh exchange unambiguously identifies whether the factory is skipped, the typed
|
||||
object exists without a body/reader, or virtual deserialization dispatch is skipped.
|
||||
|
||||
### M4 — reverse the exact dispatch contract
|
||||
|
||||
Use M3 captures and static analysis to answer all of these before enabling intervention:
|
||||
|
||||
- the exact common body-to-response-deserializer callsite;
|
||||
- the relationship between response vtable `0x18022e5b0` slot `+0x08` and the older
|
||||
message-object vtable `0x18022e598` slot `+0x20`;
|
||||
- which completion argument or object field owns the genuine reader;
|
||||
- the reader's valid synchronous lifetime;
|
||||
- whether `0x1800b8c30` executes after a successful forced parse;
|
||||
- the native transaction/game thread identity; and
|
||||
- whether the parser can be reached more than once for one response.
|
||||
|
||||
Exit: a written call contract identifies the exact hook site, preserved instructions,
|
||||
original target, arguments, ownership, thread, and resume address.
|
||||
|
||||
### M5 — behavior-preserving detour
|
||||
|
||||
Install the production-form detour at the chosen boundary but initially tail-call the
|
||||
original path unchanged. Prefer a small audited trampoline abstraction over copying the
|
||||
repository's unhook/rehook diagnostic pattern.
|
||||
|
||||
Exit: exactly one balanced entry/exit is recorded per SBC exchange; HTTP traffic, modal,
|
||||
M/B state, timing, and unrelated FUT screens remain unchanged.
|
||||
|
||||
### M6 — guarded dispatch repair
|
||||
|
||||
On the native transaction thread and only while the genuine objects are live:
|
||||
|
||||
1. require request vtable `0x18022e5c0`, response vtable `0x18022e5b0`, and status 200;
|
||||
2. require a readable reader pointer/vtable and recursion depth zero;
|
||||
3. require that this transaction has not already been parsed;
|
||||
4. call the original typed method `0x18017b2b0(response, reader)` exactly once;
|
||||
5. capture its return and the resulting M state; and
|
||||
6. resume the native completion/callback path.
|
||||
|
||||
Never run this from the deferred worker or while the SBC controller is iterating. Do not
|
||||
attempt in-place memory repair after an exception or partial parse; preserve logs and
|
||||
relaunch FIFA.
|
||||
|
||||
Exit: the deserializer is observed once, returns successfully, and native execution
|
||||
continues without gate or refresh writes.
|
||||
|
||||
### M7 — validate and commit UI state
|
||||
|
||||
Before exposing populated data, require:
|
||||
|
||||
- `M != 0` and a bounded category count;
|
||||
- category vector `begin <= end <= capacity`;
|
||||
- `(end-begin) % 0xf0 == 0` and vector length equals `WORD[M+0x50]`;
|
||||
- sane, unique category/set identifiers and bounded nested counts;
|
||||
- all native index-rebuild/finalization calls observed; and
|
||||
- no duplicate parse or partial state.
|
||||
|
||||
First allow the native callback to arm the cache. If it does not, the only fallback is
|
||||
`BYTE[B+0x28]=1` while `B+0x08==0`; never write `B+0x08` or `B+0x20`. Initially require
|
||||
the user to close/reopen SBC for refresh. Do not synthesize Scaleform events until the
|
||||
signature and ownership contract of `0x1801a4a70` are independently proven.
|
||||
|
||||
Exit: no modal; displayed categories and set counts match the served response.
|
||||
|
||||
### M8 — regression, soak, and rollback proof
|
||||
|
||||
1. Open/close SBC ten times; enter every set/challenge and return.
|
||||
2. Verify a second `/sets` response is idempotent and does not duplicate data.
|
||||
3. Smoke-test hub, club, store, squads, and normal service traffic.
|
||||
4. Repeat from two fresh launches with different ASLR slides.
|
||||
5. Soak 30–60 minutes with navigation and, if supported, repeated FUT enter/exit.
|
||||
6. Disable all SBC flags and confirm the baseline behavior returns without detours/writes.
|
||||
7. Disable `WINEDLLOVERRIDES`, restore the exact backed-up proxy if needed, and prove hard
|
||||
rollback with FIFA closed.
|
||||
|
||||
Exit: zero crashes/freezes, stable counts and memory behavior, no unrelated FUT regression,
|
||||
and both soft and hard rollback are demonstrated.
|
||||
|
||||
## Testing and build checks
|
||||
|
||||
Run at minimum:
|
||||
|
||||
```text
|
||||
cargo fmt --check
|
||||
cargo test --features fifa17
|
||||
cargo check --release --features fifa17 --target x86_64-pc-windows-gnu
|
||||
cargo build --release --features fifa17 --target x86_64-pc-windows-gnu
|
||||
```
|
||||
|
||||
Extract pure, host-testable helpers for RVA calculation, fingerprint comparison, state
|
||||
transitions, bounded vector validation, and structured event formatting. Windows calls,
|
||||
raw pointer reads, and patching should remain behind small interfaces so guard logic can be
|
||||
tested without launching FIFA.
|
||||
|
||||
## Stop conditions
|
||||
|
||||
Stop and roll back on any unknown binary fingerprint, patch-byte mismatch, wrong vtable,
|
||||
wrong thread, unexpected factory/deserializer count, recursion, invalid vector geometry,
|
||||
missing finalizer, partial parse, crash/freeze, unrelated FUT regression, or save/profile
|
||||
change. Preserve hook log, UTAS log, binary hashes, and crash evidence before relaunching.
|
||||
|
||||
## Definition of done
|
||||
|
||||
- The hook is default-off and endpoint/class-specific.
|
||||
- Exact binary and patch-site fingerprints are verified before intervention.
|
||||
- The real category deserializer runs exactly once for each intended HTTP 200 response,
|
||||
using the genuine response and reader on their native thread.
|
||||
- `M` passes structural validation and the populated SBC menu supports drill-down.
|
||||
- No communication modal appears and non-SBC FUT behavior is unchanged.
|
||||
- Two fresh ASLR-distinct launches and the soak test pass.
|
||||
- Unsetting flags restores inert behavior; removing the proxy restores the original launch.
|
||||
@@ -0,0 +1,237 @@
|
||||
# SBC Menu Render Intervention — Plan (2026-08-07)
|
||||
|
||||
**STATUS (one line): YES, WITH CAVEATS — a populated SBC menu is achievable via a
|
||||
client-side hook, but ONLY by making the game's own parser fill its store; a
|
||||
/proc/mem byte poke alone can open the menu (negative control) but renders EMPTY, and
|
||||
the one remaining un-reversed item (the SAX input-source `vtable[+0x8]` byte-yield
|
||||
contract) blocks the fully-offline populate until a served /sbs/sets response or a
|
||||
completed reader is wired.**
|
||||
|
||||
All addresses are on-disk RVAs against CardsDLL image base `0x180000000`
|
||||
(`/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll`, working copy `/tmp/fut/cardsdll.dll`).
|
||||
Live slide this session = `0x6ffe7c140000` (mapped base `0x6ffffc140000`), proven via
|
||||
FNV prologue at `0x180180d00`. Live values below are from read-only `/proc/12201/mem`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Definitive SBC data-flow
|
||||
|
||||
### Object graph
|
||||
- **A** = FUT root singleton = `*[0x1802e6398]`. Getter `0x18011a830`. A.vtable static
|
||||
`0x18021c2a0`. Live A = `0xb83e2b60` (vtable matches static — CONFIRMED).
|
||||
- **B** = SBC request/TTL gate cache = `A + 0x1f9d8`. B-getter = A.vtable[+0x4e8] =
|
||||
thunk `0x18011c1f0` (`lea rax,[rcx+0x1f9d8]; ret`). B.vtable static `0x1801fae70`
|
||||
(3 slots: dtor `0x180063040`, isValid `0x180065d40`, clear `0x180065d20`). Live B =
|
||||
`0xb8402538` (vtable matches). **B is the GATE, not the render source.**
|
||||
- **M** = SBC categories/sets store = `*(A + 0x20a68)`. Reached via A.vtable[+0x9b0] =
|
||||
lazy getter `0x18011b7d0` (if `A[+0x20a68]==0` it factory-creates an EMPTY M, type-id
|
||||
`0x13f0`, and caches it). Live M = `0x0` (never built this session — SBC menu not
|
||||
opened). **M IS the render source.**
|
||||
- The "SBC manager" is **A itself**: service-id `0xed84b12` resolver A.vtable[+0x18] =
|
||||
`0x180113f50` returns `this`, so `manager.vtable[+0x9b0] == A.vtable[+0x9b0] ==
|
||||
0x18011b7d0`. The old lead `0x1801e9010` is DEBUNKED — it is an `.rdata` function
|
||||
pointer slot (`->0x18018577a`), not a manager global.
|
||||
|
||||
### Render source (CLIENT authority)
|
||||
The SBC hub/squads controller (ctor `0x1800b5267`) caches M into `controller+0x140`
|
||||
by calling A.vtable[+0x9b0] once (`0x1800b554d`→`0x1800b5571`→store `[rsi+0x140]`),
|
||||
then registers Scaleform events `0x756c`–`0x7574`. The tile-build method (`0x1800b5e00`
|
||||
region) reads `[ctrl+0x140]=M` and at **`0x1800b5eda`** does
|
||||
`movzx ebx,WORD[M+0x50]; add bx,0x2; call [scaleform.vtable+0x58](count)` → emits
|
||||
**(category_count + 2) tiles**. This region reads `[ctrl+0x140]` seven times and reads
|
||||
B/`A+0x1fa00` **zero** times. M layout: cat count `WORD[M+0x50]`; cat vector
|
||||
`[M+0x58]..[M+0x60]` stride `0xf0`; per-cat set count `WORD[cat+0xb8]`, set vector
|
||||
`[cat+0xc0]` stride `0x3570`; secondary/featured vec `[M+0xa10]..[M+0xa18]`;
|
||||
indices at `+0x9e0/+0xa10/+0xa40`. **Correction on record:** earlier passes that
|
||||
called `B[+0x08]` the render source conflated the gate with the data source — the empty
|
||||
render was because M was null/empty, NOT because `B[+0x08]` was null.
|
||||
|
||||
### Populate path (CLIENT authority)
|
||||
The sbs/sets deserializer **`0x18017b2b0`** (rcx=this IGNORED; rdx=SAX cursor is the
|
||||
only live input) does the whole populate: fetch manager → get store M via
|
||||
`[manager.vtable+0x9b0]` (at `0x18017b327`) → clear `0x18015f3a0` → loop atom `0x6f`
|
||||
"categories": per item ctor `0x180159da0` (0xf0, vtable `0x18021b520`), cat-deser
|
||||
`0x18017ab80`, cat-finalize `0x180160e50`, APPEND `0x18015a770` (copy-ctor
|
||||
`0x18015a2b0`), dtor `0x1801105d0` → after loop rebuild indices `0x180160e00` +
|
||||
`0x180160f30` + `0x180161020` → commit `manager.vtable[+0x8]`. Always returns true.
|
||||
Set-row deser `0x18017ad60`. **Populate-target == render-source (both are M).**
|
||||
|
||||
### Prefetch gate (SERVER/front-end authority — THE WALL)
|
||||
There is **no native flag** to flip. The only native online check `0x1801642c0`
|
||||
(inside isValid) is stubbed `mov al,1; ret` — NOT the wall. The block is upstream in
|
||||
the Flash/ActionScript FUT front-end (FNV-name-hash bound; `RequestChallengeData` =
|
||||
`0x1801f9b30`, `futsbchubviewmodel` = `0x1801ee0a0` — no native xref), which refuses to
|
||||
issue `GET ut/game/fifa17/sbs/sets` offline, so deser `0x18017b2b0` never runs.
|
||||
**Newly proven:** the URL template `"ut/%s/sbs"` (`0x18021d908`) has ZERO references
|
||||
in the image (siblings `ut/%s/tournament`, `ut/%s/season` ARE referenced) — so
|
||||
**CardsDLL has no native code that self-builds/issues the sbs GET.** This kills any
|
||||
"force the req-mgr at A+0x2a0 to fetch on its own" idea. This is why the fix must be
|
||||
client-side and must FORCE the populate.
|
||||
|
||||
### Ready-arm (CLIENT authority)
|
||||
isValid `0x180065d40(B)` verified: `if !0x1801642c0() ret0` (stub→always passes);
|
||||
`cmp [rbx+0x28],0; je fail`; **`cmp QWORD[rbx+0x8],0; je 0x180065d75` → returns 1
|
||||
immediately (short-circuit)**; else QueryPerformanceCounter (`0x1801e50c0`) and compare
|
||||
`[rbx+0x20]` deadline. Normally B is armed by the completion callback `0x1800b8c30`
|
||||
(subscribed in svc ctor `0x1800b5765` via `manager.vtable[+0xa90]`) through the generic
|
||||
cache copy-assign `0x1800c21a0` (sets B+0x08=collection, B+0x20=deadline, B+0x28=1).
|
||||
Offline that callback never fires (no response). Live: `B[+0x08]=0`, `B[+0x28]=0`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Chosen minimal intervention and WHY
|
||||
|
||||
**Reuse the client's own parser; do NOT hand-build structs; arm ONLY `B[+0x28]`.**
|
||||
|
||||
Two tiers, safest-first:
|
||||
|
||||
- **Tier-0 (negative control — proves the gate):** write ONLY `BYTE[B+0x28]=1`.
|
||||
isValid short-circuits (B+0x08==0 branch) → menu OPENS instead of the error modal
|
||||
(`0x18016c330`), but renders EMPTY (M is null/empty). Do NOT write `B+0x08` or
|
||||
`B+0x20` — pointing B+0x08 at a collection forces isValid into the QPC-deadline
|
||||
branch, and with the live-stale deadline (`0xf10fb8cb9`) the gate SHUTS → modal, i.e.
|
||||
it DEFEATS the fix. This is the load-bearing correction from adversarial verification.
|
||||
|
||||
- **Tier-1 (real fix — populates M):**
|
||||
- **Preferred (Option 1, cleanest, zero forged state):** inject a canned
|
||||
`/sbs/sets` JSON response at the message-receive layer so the game builds the
|
||||
response-msg (ctor `0x18017b1c0`, vtable `0x18022e598`, deser slot +0x20 =
|
||||
`0x18017b2b0`), seats a genuine SAX cursor, its OWN chain populates M, and the
|
||||
native completion callback `0x1800b8c30` arms B for you. The bridge/core serves the
|
||||
JSON. Nothing forged.
|
||||
- **Fallback (Option 2):** from the hook, stand up a real SAX cursor over canned JSON
|
||||
(ctx `0x1801c63e0` + lexer `0x1801c8060` + an input-source whose `vtable[+0x8]`
|
||||
yields bytes), call deser `0x18017b2b0(rcx=ignored, rdx=cursor)`, then arm ONLY
|
||||
`BYTE[B+0x28]=1`. **Blocker:** the input-source `vtable[+0x8]` byte-yield contract
|
||||
is the ONE un-reversed item — a cold call with a null-source cursor CLEARS M
|
||||
(`0x18015f3a0`) then byte-scans a garbage pointer (`mov rdi,[rdi]` ~`0x18017b353`)
|
||||
→ wipes state + segfault. So Option 2 is NOT safe to run until the reader is
|
||||
reversed.
|
||||
|
||||
**Why not hand-build:** feeding `0x18015a770` a hand-built 0xf0 category (with nested
|
||||
0x3570 set records / EASTL sub-vectors) is the highest crash risk — the copy-ctor
|
||||
`0x18015a2b0` deep-copies inner sub-vectors; any bad begin/end/cap → heap corruption.
|
||||
The parser writes the correct geometry AND runs the index-rebuild finalizers that
|
||||
hand-built appends get wrong. Ruled out.
|
||||
|
||||
**Refresh:** after M is populated, fire refresh events `0x756c`–`0x7574` (or re-open the
|
||||
menu) so `0x1800b5eda` re-reads `WORD[M+0x50]`.
|
||||
|
||||
---
|
||||
|
||||
## 3. STAGED MORNING TEST PLAN (safest-first)
|
||||
|
||||
Precondition: FIFA at the FUT hub with CardsDLL loaded. Rollback for EVERY step =
|
||||
**relaunch FIFA** (all effects are volatile — single-byte poke or in-session hook state,
|
||||
cleared on restart). NEVER run `--apply` while the SBC menu is open/mid-iterate.
|
||||
|
||||
### Step 1 — Dry-run read confirm (ZERO writes)
|
||||
```
|
||||
python3 /home/alex/Documents/OpenFUT/fifa17-recon/tools/sbc_hook_poke.py
|
||||
```
|
||||
Expect: CONTROL FNV MATCH; A vtable match; B offset decoded live = `0x1f9d8`; B/A vtables
|
||||
match statics; `B+0x28=0`; `M=*(A+0x20a68)=0` (until SBC menu opened once).
|
||||
PASS = addresses match the model. Rollback: none needed (read-only).
|
||||
|
||||
### Step 2 — Review the DLL populate spec (ZERO writes)
|
||||
```
|
||||
python3 /home/alex/Documents/OpenFUT/fifa17-recon/tools/sbc_hook_poke.py --spec
|
||||
```
|
||||
Expect: printed injected-DLL spec (Option 1 preferred, Option 2 fallback). Read-only.
|
||||
|
||||
### Step 3 — Negative control (Tier-0, ONE byte write) — proves the GATE
|
||||
With the SBC menu **CLOSED**:
|
||||
```
|
||||
python3 /home/alex/Documents/OpenFUT/fifa17-recon/tools/sbc_hook_poke.py --apply
|
||||
```
|
||||
Writes exactly `BYTE[B+0x28]=1` (re-proves slide+vtables at write time; aborts on any
|
||||
mismatch; hard-refuses to write B+0x08/B+0x20). Then re-open the SBC menu.
|
||||
Expect: menu OPENS, no error modal, ~2 empty/placeholder tiles. This proves the gate +
|
||||
isValid short-circuit LIVE — it does NOT prove data. If it CRASHES: stop — B
|
||||
resolution/slide is wrong. Rollback: relaunch FIFA (byte clears on restart).
|
||||
|
||||
### Step 4 — Real fix (Tier-1) — proves the DATA (NOT for a blind run)
|
||||
Do this only after the DLL populate is implemented. Preferred: bring up the bridge/core
|
||||
`/sbs/sets` responder and let Option 1 (message-layer injection) drive the native chain;
|
||||
the completion callback arms B and M fills. Then the same gate opens a POPULATED menu
|
||||
(N+2 tiles). The hook module scaffold is `openfut-hook/src/sbc_hook.rs` — Tier-1
|
||||
`populate_m()` is present but deliberately refuses to call the deser until the SAX
|
||||
input-source reader is reversed (else it clears M and crashes). Build (when ready):
|
||||
```
|
||||
cd /home/alex/Documents/OpenFUT/openfut-launcher/openfut-hook && \
|
||||
cargo build --release --features fifa17 --target x86_64-pc-windows-gnu
|
||||
```
|
||||
Deploy as `version.dll` per launcher setup. Env gates (all default OFF):
|
||||
`OPENFUT_SBC_HOOK=1` (read-only resolve+log), `OPENFUT_SBC_ARM_ONLY=1` (Tier-0),
|
||||
`OPENFUT_SBC_POPULATE=1` (Tier-1, currently logs the blocker and returns).
|
||||
Rollback: unset env vars and relaunch FIFA.
|
||||
|
||||
### Step 5 — Cleanup
|
||||
Unset all `OPENFUT_SBC_*` env vars; relaunch FIFA to a clean state.
|
||||
|
||||
---
|
||||
|
||||
## 4. Crash-risk assessment
|
||||
|
||||
1. **Cold-calling `0x18017b2b0` without a real seated cursor** — CLEARS M
|
||||
(`0x18015f3a0`) first, then `mov rdi,[rdi]` byte-scan on a garbage ptr → wipes
|
||||
state + segfault. HIGHEST. Tier-1 code refuses this until the reader is reversed.
|
||||
2. **Writing `B+0x08`/`B+0x20`** — forces isValid into the QPC-deadline branch; stale
|
||||
deadline → gate SHUTS (modal), or garbage-ptr iterate crash. Self-defeating.
|
||||
Tool/code write ONLY `B+0x28`.
|
||||
3. **Populate off the game thread / mid-iterate** — lazy getter allocates on game heap,
|
||||
appender mutates EASTL vectors; a foreign thread races the allocator/menu iterate →
|
||||
heap corruption. Tier-1 must run on the game/message-pump thread with the menu closed.
|
||||
4. **Skipping the index-rebuild finalizers** (`0x180160e00/0x180160f30/0x180161020`)
|
||||
after append → stale `+0x9e0/+0xa10/+0xa40` indices → by-index getter `0x180160a80`
|
||||
reads OOB → crash/garbage tiles.
|
||||
5. **`WORD[M+0x50]` > actual 0xf0-stride entries** → tile loop walks past vector end
|
||||
(OOB read).
|
||||
6. **Hand-built 0xf0/0x3570 structs fed to `0x18015a770`** — copy-ctor `0x18015a2b0`
|
||||
deep-copies inner EASTL sub-vectors; bad begin/end/cap → heap corruption. Avoid.
|
||||
7. **No refresh after populate** (non-crash) — controller keeps the cached empty M at
|
||||
`ctrl+0x140`; `0x1800b5eda` won't re-run → still 2 placeholder tiles. Fire
|
||||
`0x756c`–`0x7574` or re-open.
|
||||
8. **Manager/store null** — deser does `mov rax,[rbx]` on the manager; registry lookup
|
||||
(hashes `0xed84b11`/`0xed84b12`) returning null → null-deref. Live registry
|
||||
`*[0x1802c2988]` non-null, so low risk; hook must still null-check M/store.
|
||||
|
||||
Tier-0 (single `B+0x28=1` write, B+0x08 left 0) is the verified-SAFE case: isValid
|
||||
short-circuits to 1, renders empty, no crash; bg-thread-tolerant like the /proc poke.
|
||||
|
||||
---
|
||||
|
||||
## 5. Poke tool + DLL-spec locations
|
||||
|
||||
- Poke tool (read-only default; `--spec`; `--apply` = ONLY `BYTE[B+0x28]=1`):
|
||||
`/home/alex/Documents/OpenFUT/fifa17-recon/tools/sbc_hook_poke.py`
|
||||
- Negative-control byte poke (older, triple-guarded):
|
||||
`/home/alex/Documents/OpenFUT/fifa17-recon/tools/sbc_populate_poke.py`
|
||||
- Slide/read template + FNV control proof:
|
||||
`/home/alex/Documents/OpenFUT/fifa17-recon/tools/gate_byte_probe.py`
|
||||
- DLL integration spec (RVA math, object graph, gate disasm, function-signature table,
|
||||
3 intervention tiers, 8-item crash register, staged test plan):
|
||||
`/home/alex/Documents/OpenFUT/fifa17-recon/docs/sbc-hook-dll-spec.md`
|
||||
- Injected-DLL module (fifa17-only; Tier-0 live, Tier-1 scaffolded/refusing):
|
||||
`/home/alex/Documents/OpenFUT/openfut-launcher/openfut-hook/src/sbc_hook.rs`
|
||||
(wired via `lib.rs` `#[cfg(feature="fifa17")] mod sbc_hook;` + `fifa17.rs`
|
||||
`crate::sbc_hook::install();`)
|
||||
- Atoms table: `/home/alex/Documents/OpenFUT/fifa17-recon/docs/fut_atoms.tsv`
|
||||
|
||||
---
|
||||
|
||||
## Client-vs-server authority boundaries (flagged)
|
||||
|
||||
- **RENDER (M, tiles at `0x1800b5eda`)** — CLIENT. The client draws tiles solely from
|
||||
M; the server never touches this. Fix is client-side.
|
||||
- **POPULATE (deser `0x18017b2b0` → M)** — CLIENT parser, SERVER-fed data. The parser
|
||||
is native and reusable; the DATA it needs (`/sbs/sets` JSON) is a server response.
|
||||
Preferred fix has the bridge/core supply that JSON so the client parses it natively.
|
||||
- **PREFETCH GATE (issue `GET sbs/sets`)** — SERVER/front-end. THE WALL. No native
|
||||
flag; the SWF/ActionScript front-end refuses to request offline, and CardsDLL has no
|
||||
native code that issues the GET (`ut/%s/sbs` unreferenced). This cannot be fixed
|
||||
server-side by responding — the request is never sent. The hook must force the
|
||||
populate (inject the response at the message layer or drive the parser).
|
||||
- **READY-ARM (`B[+0x28]`, callback `0x1800b8c30`/commit `0x1800c21a0`)** — CLIENT.
|
||||
Normally armed by the completion callback (server-response-driven); offline the hook
|
||||
arms it (Tier-0 byte, or Option 1 lets the native callback arm it).
|
||||
@@ -0,0 +1,138 @@
|
||||
# SBC "problem communicating with the FIFA Ultimate Team servers" — definitive analysis
|
||||
|
||||
**Date:** 2026-08-07
|
||||
**Binary under study:** `/tmp/fut/cardsdll.dll` (on-disk PE, image base `0x180000000`; copy of `/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll`)
|
||||
**Method:** clean-room, read-only. On-disk `objdump` re-verified in this pass; live values quoted from prior read-only `/proc/<pid>/mem` reads (pid 12201, slide `0x6ffe7c140000`, FNV control MATCH). No memory was written; FIFA was not touched.
|
||||
|
||||
---
|
||||
|
||||
## VERDICT (one line)
|
||||
|
||||
**The SBC modal is a CLIENT-SIDE, per-feature completion-path defect — the FUT client never re-arms a fetch/re-render for `sbs/sets` the way it does for the hub — so NO server response can cure it; the only offline lever is a client-memory patch, and the clean single-byte patch (`model+0x1fa00 = 1`) only SUPPRESSES the modal by forcing the completion predicate true, rendering from an empty, never-populated cache. It is NOT the go-online wall.**
|
||||
|
||||
---
|
||||
|
||||
## 1. What the SBC completion predicate actually checks (CONFIRMED on-disk)
|
||||
|
||||
The SBC menu entry runs a completion continuation whose gate is the shared predicate **`0x180065d40`**, called as `[cache_vtable+0x08]`. Re-disassembled this pass, byte-for-byte:
|
||||
|
||||
```
|
||||
180065d40 call 0x1801642c0 ; online/liveness sub-check
|
||||
180065d4e test al,al
|
||||
180065d50 je fail
|
||||
180065d52 cmp byte [rbx+0x28],0 ; <-- THE GATE: "value ready" flag
|
||||
180065d56 je fail
|
||||
180065d58 cmp qword [rbx+0x8],0 ; pending-op ptr
|
||||
180065d5d je pass (mov al,1) ; empty-collection shortcut -> success
|
||||
180065d5f lea rcx,[rsp+0x38]
|
||||
180065d64 call QueryPerformanceCounter ; [rip]->0x1801e50c0
|
||||
180065d6a mov rax,[rbx+0x20] ; QPC deadline
|
||||
180065d6e sub rax,[rsp+0x38]
|
||||
180065d73 js fail ; deadline passed -> fail
|
||||
180065d75 mov al,1 ; pass
|
||||
...
|
||||
180065d7d xor al,al ; fail
|
||||
```
|
||||
|
||||
Reduces to: `subcheck() && byte[cache+0x28]!=0 && (qword[cache+0x08]==0 || deadline[cache+0x20] not yet past)`.
|
||||
|
||||
- **The online/liveness sub-check `0x1801642c0` is stubbed OUT.** On-disk bytes are `b0 01 c3` = `mov al,1; ret` — always true, in the shipped file (not a live loader patch). **This is the reason SBC is NOT the go-online wall** (see §5).
|
||||
- `cache` (`rbx`) is an **embedded sub-object of the FUT root singleton** `A = *[0x1802e6398]`, selected by a vtable thunk (see §2). Its `+0x28` byte is a "value-ready" flag (init 0 by ctor `0x180062460`); `+0x08` is a pending-op pointer; `+0x20` is a QPC deadline. This is a copyable future/async-result value type. **The predicate never reads the parsed SBC categories, HTTP status, session, or any live-connection boolean.**
|
||||
|
||||
> **AUTHORITY BOUNDARY:** everything the predicate reads lives inside client process memory (`A+…`). Nothing in the `sbs/sets` HTTP response is an input to it. This is a **client-authority** decision end to end.
|
||||
|
||||
---
|
||||
|
||||
## 2. Why hub passes but `sbs/sets` fails (CORRECTED after adversarial verification)
|
||||
|
||||
Both features run the **same predicate function** `0x180065d40`, but on **different embedded caches**, reached through **different per-response-class continuations**. That structural divergence is real and confirmed. **The originally-stated reason ("hub passes because its cache `+0x28` is set") is WRONG** and is corrected here — corroborated by a live measurement (HUB cache `+0x28 = 0` while the hub is displayed with no modal) and by the on-disk FALSE-branch disassembly gathered this pass.
|
||||
|
||||
### The two continuations, side by side (on-disk, this pass)
|
||||
|
||||
| | SBC (`FutLoadSetTypesServerResponse`) | HUB (`FutGetHubDataServerResponse`) |
|
||||
|---|---|---|
|
||||
| continuation | `0x180154860` | `0x180173770` |
|
||||
| get singleton A | `call 0x18011a830` (`mov rax,[0x1802e6398]`) | same |
|
||||
| select cache | `call [rdx+0x4e8]` → thunk `0x18011c1f0` = `lea rax,[rcx+0x1f9d8]` → **SBC cache A+0x1f9d8** | `call [rdx+0x1f8]` → thunk `0x18011a810` = `lea rax,[rcx+0x1fd70]` → **HUB cache A+0x1fd70** |
|
||||
| predicate | `call [rdx+0x08]` = `0x180065d40` | **same** `0x180065d40` |
|
||||
| on TRUE (jne) | render `0x18015491a → 0x180154600` | render `0x18017383d → 0x1801735e0` |
|
||||
| **on FALSE** | `lea rdx,[rbp-0x9]` (descriptor `0x18020a8b8`); **`call 0x18016c330`**; `jmp` return | **`call 0x1801213b0` (state reset)**; `lea 0x1801736f0` (continuation fn); **`call 0x18011f8e0` (register completion closure)**; `lea 0x18022cd30` (descriptor); **`call 0x18016c330`**; **`call 0x18011f900` (cleanup)** |
|
||||
|
||||
### What this proves
|
||||
|
||||
1. **`0x18016c330` is NOT an SBC-only "modal" function.** The HUB continuation calls the very same `0x18016c330` (at `0x18017382c`) on its own not-ready branch. It is a shared, descriptor-parameterized async dispatcher; SBC passes descriptor `0x18020a8b8`, hub passes `0x18022cd30`.
|
||||
|
||||
2. **At idle both predicates return FALSE.** Live: HUB cache `A+0x1fd70+0x28 = 0` **and** SBC cache `A+0x1f9d8+0x28 = 0`, both `+0x08 = 0`. The hub is on screen with no modal *while its own predicate would return FALSE*. So "hub `+0x28` is set" is false; a set flag is not what makes the hub pass.
|
||||
|
||||
3. **The real asymmetry is the FALSE-branch work.** On not-ready the HUB continuation **resets its request-state region** (`0x1801213b0`), **registers a completion closure** (`0x18011f8e0`, continuation `0x1801736f0`) so the arriving response re-runs the continuation and re-renders, then cleans up (`0x18011f900`). It is a proper get-or-fetch: cache-miss → (re)issue request → render on completion. **The SBC continuation does NONE of that** — it fires the dispatcher once with delegate `0x180154590`/descriptor `0x18020a8b8` and returns. It never re-arms a fetch and never wires the `sbs/sets` response back into a re-render.
|
||||
|
||||
**Conclusion:** hub and SBC diverge at the cache-selection call site (`[rdx+0x1f8]` vs `[rdx+0x4e8]`, one instruction apart), and — decisively — in the not-ready handling. The modal is produced **downstream in the SBC dispatched path** (dispatcher `0x18016c330` + delegate `0x180154590`), because the SBC feature is wired as a one-shot with no re-fetch/re-render, whereas the hub is wired as a self-rearming get-or-fetch. It is **not** decided by cache selection alone, **not** by the shared predicate, and **not** by the `+0x28` byte value at idle.
|
||||
|
||||
---
|
||||
|
||||
## 3. VERDICT by route — is SBC beatable, and how?
|
||||
|
||||
| Route | Outcome | Why |
|
||||
|---|---|---|
|
||||
| **A. Server response field / header / status** | **RULED OUT — no offline fix here** | No field in the `sbs/sets` body reaches the predicate (client-authority §1). Deeper: the SBC continuation never registers a completion closure to consume the response and re-render, so *even a perfect response is dropped on the floor*. The deserializer `0x18017b2b0` returning TRUE is genuinely irrelevant. |
|
||||
| **B. Client memory byte patch** `model+0x1fa00 = 1` | **Suppresses the modal, but empty menu — cosmetic** | Forces predicate TRUE → routes to the SBC render branch `0x18015491a → 0x180154600`, which reads the embedded SBC cache. That cache was never populated (`+0x08 == 0`, empty collection), so the likely result is an empty / non-functional SBC screen, not populated SBCs. **Untested under the read-only rule.** |
|
||||
| **C. Config `FUT/SBC_USE_STUBS`** (rdata `0x1802270f8`) | **Not the gate** | Read at the deser top only; the normal (off) path already runs. Flipping it does not touch `+0x28` or the continuation wiring. |
|
||||
| **D. "Needs the go-online wall solved"** | **REFUTED** | The only connection-like sub-check on this path (`0x1801642c0`) is stubbed to always-true on-disk. SBC is blocked by local per-feature completion wiring, not by the reconnect gate. See §5. |
|
||||
| **E. Client CODE patch of the SBC FALSE-branch** | **The only route to a *functional* SBC menu** | Make `0x180154860`'s not-ready branch replicate the hub's sequence: state reset `0x1801213b0` + register completion closure `0x18011f8e0`/`0x1801736f0` + dispatch + cleanup `0x18011f900`, so the `sbs/sets` response is fetched and rendered. This is a code patch, not a byte flip and not a server change. Out of scope for a server-side preservation fix; a client-side authority modification. |
|
||||
|
||||
**Bottom line:** there is **no server-side fix**. SBC is "beatable" only in the client-authority sense — either cosmetically (byte B, hides the modal over an empty menu) or functionally (route E, a code patch replicating the hub's re-arm). Neither is a change our offline server can make.
|
||||
|
||||
---
|
||||
|
||||
## 4. Memory patch details (if used) — flagged CLIENT-SIDE AUTHORITY
|
||||
|
||||
> **CLIENT-SIDE AUTHORITY — this is a modification of the FIFA client's own process memory, not an OpenFUT server response. It changes what the client decides, and it violates the current read-only rule; it is documented for completeness, not endorsed as the fix.**
|
||||
|
||||
- **Cosmetic modal-suppression (route B):**
|
||||
- **Absolute displacement into FUT root singleton:** `A + 0x1f9d8 + 0x28` = **`model + 0x1fa00`**, where `A = *[0x1802e6398]`.
|
||||
- **Live absolute (pid 12201 snapshot):** `0xb8402538 + 0x28 = 0xb8402560`.
|
||||
- **Value:** write `0x01` (one byte).
|
||||
- **Effect:** predicate `0x180065d40` short-circuits at `cmp byte[rbx+0x28],0` → with `+0x08==0` the empty-collection shortcut returns TRUE → continuation `jne 0x18015491a` renders. **Modal gone; SBC cache empty → expect an empty/possibly-broken menu.** Not verified (read-only).
|
||||
- **Persistence:** the object is embedded in the singleton (singleton lifetime). The SBC path calls only `[vt+0x08]`; nothing on this path calls the invalidator `[vt+0x10]=0x180065d20`, so a write should persist across menu re-entry (inferred from structure, not demonstrated).
|
||||
|
||||
- **Functional fix (route E)** requires a `.text` patch to the SBC continuation, not a data byte — see §3 row E. Do not confuse the two.
|
||||
|
||||
---
|
||||
|
||||
## 5. Relationship to the online-modes / go-online-wall finding
|
||||
|
||||
SBC is **not** the same wall as online Draft's "PRESS Q TO RECONNECT":
|
||||
|
||||
- The single connection-like sub-check reachable from the SBC predicate, `0x1801642c0`, is compiled out (`mov al,1; ret`) in the shipped binary. The SBC gate therefore encodes **no** unmet network condition — it is a purely local completion-wiring problem.
|
||||
- The online modes differ structurally: their gate keeps a real pending network op at `+0x08` and/or a non-stubbed sub-check, so their predicate encodes a network state a local byte-flip cannot satisfy. That is why the online wall is not beatable by a byte and SBC's modal is (cosmetically).
|
||||
- This is consistent with the prior **"refusing modes = no server fix"** finding: no field, count, header, or status in any HTTP response flips the client-side completion state for these features. SBC extends that finding with the precise mechanism — the client never re-arms the `sbs/sets` fetch/re-render at all.
|
||||
|
||||
---
|
||||
|
||||
## Appendix — confirmed addresses (image base `0x180000000`)
|
||||
|
||||
| Symbol | Address | Note |
|
||||
|---|---|---|
|
||||
| FUT root singleton getter | `0x18011a830` | `mov rax,[0x1802e6398]; ret` |
|
||||
| FUT root singleton ptr | `[0x1802e6398]` | live `A = 0xb83e2b60` |
|
||||
| FUT root vtable (static) | `0x18021c2a0` | |
|
||||
| SBC cache selector thunk | `0x18011c1f0` | `lea rax,[rcx+0x1f9d8]` (slot `A.vt+0x4e8`) |
|
||||
| HUB cache selector thunk | `0x18011a810` | `lea rax,[rcx+0x1fd70]` (slot `A.vt+0x1f8`) |
|
||||
| SBC cache | `A+0x1f9d8` | vtable `0x1801fae70`; live `0xb8402538` |
|
||||
| HUB cache | `A+0x1fd70` | vtable `0x18021c1e0` |
|
||||
| shared predicate `isValid` | `0x180065d40` | `cache.vt+0x08` for both |
|
||||
| stubbed online sub-check | `0x1801642c0` | `b0 01 c3` = `mov al,1; ret` |
|
||||
| cache ctor / copy-ctor | `0x180062460` / `0x1800c21f3` | init `byte[+0x28]=0` |
|
||||
| invalidator | `0x180065d20` | `cache.vt+0x10`; not called on SBC path |
|
||||
| SBC continuation | `0x180154860` | class `RS4:FutLoadSetTypesServerResponse` (str `0x1802270b8`, vt row `0x180227090`) |
|
||||
| HUB continuation | `0x180173770` | class `RS4:FutGetHubDataServerResponse` (str `0x18022ce40`, vt row `0x18022ce18`) |
|
||||
| shared async dispatcher | `0x18016c330` | called by BOTH FALSE-branches (SBC `0x180154913`, HUB `0x18017382c`) |
|
||||
| SBC delegate / descriptor | invoke `0x180154590` / desc `0x18020a8b8` | |
|
||||
| HUB re-arm: state reset | `0x1801213b0` | HUB-only, `0x1801737bd` |
|
||||
| HUB re-arm: register closure | `0x18011f8e0` (cont. `0x1801736f0`) | HUB-only, `0x180173815` |
|
||||
| HUB re-arm: cleanup | `0x18011f900` | HUB-only, `0x180173836` |
|
||||
| SBC render branch (on TRUE) | `0x18015491a → 0x180154600` | reads empty SBC cache |
|
||||
| `sbs/sets` deserializer | `0x18017b2b0` | returns TRUE unconditionally (`mov al,1 @0x18017b751`); irrelevant to predicate |
|
||||
| QueryPerformanceCounter import | `0x1801e50c0` | |
|
||||
|
||||
**Which prior conclusion won:** the structural divergence (same predicate, different cache, different continuation; online sub-check stubbed; not server-fixable) is upheld. The specific pass/fail *reason* is corrected: it is the **FALSE-branch re-arm asymmetry**, not a set `+0x28` byte and not an SBC-exclusive `0x18016c330`.
|
||||
@@ -0,0 +1,211 @@
|
||||
# FIFA 17 SBC response reconciliation
|
||||
|
||||
**Verdict:** the live client receives HTTP 200 for `GET /ut/game/fifa17/sbs/sets`, but the
|
||||
typed `FutSBCLoadCategoryDetailsServerResponse` deserializer is not invoked. The evidence
|
||||
does **not** identify a server-controlled header, envelope field, or correlation value that
|
||||
can fix this. The previous `0x180154860` “SBC continuation” diagnosis was based on the wrong
|
||||
request class and is retracted.
|
||||
|
||||
## Scope and authority
|
||||
|
||||
This pass used only:
|
||||
|
||||
- the shipped `CardsDLL_Win64_retail.dll` copied to `/tmp/fut/cardsdll.dll`;
|
||||
- read-only `/proc/<pid>/mem` access to the running game;
|
||||
- the local OpenFUT request log; and
|
||||
- existing clean-room notes and scripts in this repository.
|
||||
|
||||
No game memory was written, no breakpoint was inserted, and no service or game process was
|
||||
restarted during the measurement.
|
||||
|
||||
## Fresh live observation
|
||||
|
||||
The control run used fresh FIFA process **PID 59054**. The CardsDLL mapping resolved to
|
||||
`0x6ffffc140000`, giving slide `0x6ffe7c140000`. Bytes at static control function
|
||||
`0x180180d00` matched the on-disk DLL, proving the mapping/slide before data reads.
|
||||
|
||||
At the FUT hub, before opening SBC:
|
||||
|
||||
- `A = *[0x1802e6398] = 0xb78f7c50`;
|
||||
- `M = *(A+0x20a68) = 0`;
|
||||
- hub cache byte `*(A+0x1fd70+0x28) = 1` (fresh hub response ready); and
|
||||
- SBC cache byte `*(A+0x1f9d8+0x28) = 0`.
|
||||
|
||||
The user then opened the SBC tile. The real client exchange was:
|
||||
|
||||
```text
|
||||
[10:20:20] GET /ut/game/fifa17/sbs/sets
|
||||
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
X-UT-SID: OPENFUT-SID-0000000000000001
|
||||
Accept-Encoding: gzip
|
||||
-> 200 {"categories":[...]}
|
||||
```
|
||||
|
||||
The game displayed “There was a problem communicating with the FIFA Ultimate Team servers.”
|
||||
With that modal still open, the same slide was re-proved and `M` was still exactly zero.
|
||||
|
||||
### What `M == 0` proves
|
||||
|
||||
The typed `/sets` deserializer is `0x18017b2b0`. At `0x18017b309`–`0x18017b327` it obtains
|
||||
the FUT root and calls vtable slot `+0x9b0`, the lazy getter `0x18011b7d0`. That getter
|
||||
allocates and stores `A+0x20a68` before the deserializer examines the root object or the
|
||||
`categories` key.
|
||||
|
||||
Consequently:
|
||||
|
||||
- valid JSON would leave `M` non-null;
|
||||
- malformed or empty JSON reaching this function would also leave `M` non-null; and
|
||||
- `M == 0` after the completed HTTP transaction means `0x18017b2b0` was not invoked.
|
||||
|
||||
The normal reset of `M` is `0x180114ee0`; its observed use belongs to broad FUT-root
|
||||
initialization/reset work, not the `/sets` completion path. There is no evidence that the
|
||||
deserializer ran and then immediately cleared `M` during this transaction.
|
||||
|
||||
## Correct class map
|
||||
|
||||
Three classes were conflated in earlier notes:
|
||||
|
||||
| Function/class | Proven URI | Role |
|
||||
|---|---|---|
|
||||
| `FutSBCLoadCategoryDetailsServerResponse`, request URI builder `0x18017a980`, factory `0x18017aa10`, response deser `0x18017b2b0` | `/sets` under the `ut/%s/sbs` base | Initial category/set list; this is the live failing request |
|
||||
| `FutSBCSetDataServerResponse`, factory `0x18016fca0`, deser `0x18016fe90` | `/squadBuildingSets` (`0x18022bd88`) | Parses `reset`; not the observed `/sbs/sets` request |
|
||||
| `FutLoadSetTypesServerResponse`, deser `0x180154990` | `/challenge/%d/squad` (`0x1802270e0`) | Parses `challengeId`, `playerRequirements`, and `squad`; later challenge flow |
|
||||
|
||||
This corrects two prior claims:
|
||||
|
||||
1. `FutSBCSetDataServerResponse` does **not** share the literal `/sets` URI in this binary;
|
||||
its URI string is `/squadBuildingSets`.
|
||||
2. `0x180154860` is not a dedicated completion continuation for the initial category-list
|
||||
request. `0x180154830` is a generic callback thunk used by multiple request classes, while
|
||||
the nearby `0x180154990` parser and `/challenge/%d/squad` URI belong to
|
||||
`FutLoadSetTypesServerResponse`.
|
||||
|
||||
Therefore the earlier hub-versus-`0x180154860` comparison contrasted the hub with a later
|
||||
challenge-squad operation, not with `GET /sbs/sets`. Its proposed “copy the hub re-arm path”
|
||||
fix is unsupported for the category-list failure.
|
||||
|
||||
## What the generic completion code actually checks
|
||||
|
||||
The shared request completion routine `0x18016cca0`:
|
||||
|
||||
1. calls request vtable slot `+0x80` at `0x18016cd32` to create the class-selected typed
|
||||
response object;
|
||||
2. stores the received status at request offset `+0x48` (`0x18016cd3d`); and
|
||||
3. compares it with decimal 200 at `0x18016cdd0`.
|
||||
|
||||
Exactly 200 takes the success branch to `0x18016d0b9`. Non-200 status invokes the error
|
||||
translation path through request slot `+0x60` first. Response construction is selected by
|
||||
the request vtable; it is not selected by an HTTP response header or a JSON envelope field.
|
||||
|
||||
No pre-deserialization branch found in this path reads `Content-Type`, a request/correlation
|
||||
ID, the `X-UT-SID` response header, or a top-level JSON key. The live server already supplies
|
||||
the one proven transport-level success input: status 200.
|
||||
|
||||
## Hub comparison
|
||||
|
||||
The fresh hub response was consumed successfully and set the hub cache byte to one. After
|
||||
the subsequent navigation its resting value returned to zero. The SBC cache byte remained
|
||||
zero. This confirms that cache `+0x28` is transient async-result/TTL state; a later resting
|
||||
zero does not establish which completion branch ran.
|
||||
|
||||
The previous report's live snapshot—where both values were zero long after the requests—was
|
||||
therefore insufficient to infer the hub/SBC divergence. The fresh before/after measurement
|
||||
supersedes it.
|
||||
|
||||
## Server-fixability verdict
|
||||
|
||||
**Not demonstrated.** In particular:
|
||||
|
||||
- changing the category JSON cannot make the typed parser start, because the lazy store is
|
||||
allocated before any JSON key is inspected;
|
||||
- the server already returns the proven success status, 200;
|
||||
- request-class/response-class selection is client-owned; and
|
||||
- no header, envelope, or correlation field was found feeding a pre-parser decision.
|
||||
|
||||
This does not mathematically prove that no transport variation could ever affect the client.
|
||||
It does prove that the specific server-fix candidates proposed by the killed workflow were
|
||||
speculative and had no reading instruction behind them.
|
||||
|
||||
## Exact remaining unknown and next measurement
|
||||
|
||||
The unresolved boundary is between:
|
||||
|
||||
```text
|
||||
ProtoHttp completion with status 200
|
||||
-> class-selected response object creation
|
||||
-> delivery of response bytes/SAX cursor
|
||||
-> response vtable +0x08 (`0x18017b2b0`)
|
||||
```
|
||||
|
||||
The next useful experiment is transient tracing of calls—not another resting-state scan.
|
||||
Instrument, in a disposable/local diagnostic build or a non-mutating tracing facility:
|
||||
|
||||
- request factory `0x18017aa10`;
|
||||
- typed deserializer `0x18017b2b0`;
|
||||
- generic completion entry `0x18016cca0` and its status at `0x18016cdd0`; and
|
||||
- the generic response-body/SAX dispatch site that calls response vtable slot `+0x08`.
|
||||
|
||||
Record whether the factory is called, whether it returns an object with vtable
|
||||
`0x18022e5b0`, and whether a body/SAX object is delivered. That separates three remaining
|
||||
client-side possibilities: wrong request instance despite the URI, typed object created but
|
||||
body not attached, or body attached but virtual deserialization dispatch skipped.
|
||||
|
||||
Until that transient trace exists, the defensible implementation direction remains the
|
||||
client-side hook described in `docs/sbc-hook-dll-spec.md`, but its rationale must be stated
|
||||
as “native category deserializer is not reached,” not the retracted `0x180154860`
|
||||
hub-rearm theory.
|
||||
|
||||
## 2026-08-07 passive-trace result: deserialization is proven
|
||||
|
||||
The first gated passive client trace supersedes the final inference above. During exactly
|
||||
one SBC navigation, with every mutation feature disabled, the hook recorded:
|
||||
|
||||
```text
|
||||
SBC_TRACE: factory entry=1 exit=1 tid=652 this=0xb80cd910 result=0x7a99178;
|
||||
deser entry=1 exit=1 tid=652 this=0x7a99178 reader=0x7fcff7f8 result=true
|
||||
```
|
||||
|
||||
The matching UTAS request occurred at `11:09:01`: `GET /ut/game/fifa17/sbs/sets` returned
|
||||
HTTP 200 with one category and two sets. No degraded hook state was reported, and FIFA
|
||||
remained alive until the operator closed it after the single permitted attempt.
|
||||
|
||||
This proves all of the following for the observed request:
|
||||
|
||||
- the category response factory is called exactly once and returns a non-null object;
|
||||
- the native category deserializer is called exactly once on that same object;
|
||||
- the body reader is non-null;
|
||||
- deserialization returns success (`true`); and
|
||||
- both calls return normally on the same native thread.
|
||||
|
||||
Therefore the earlier `M == 0` resting snapshot did not prove that `0x18017b2b0` was
|
||||
skipped. The failure boundary is now strictly **after successful native deserialization**.
|
||||
The next measurement must trace the response object's post-deserializer completion,
|
||||
ownership handoff, and publication into the SBC UI/cache collection. Repeating the factory
|
||||
or deserializer trace will not add useful information.
|
||||
|
||||
## 2026-08-07 post-deserializer handoff trace
|
||||
|
||||
A second one-shot run combined the factory/deserializer probes with atomic replacements of
|
||||
the category request vtable slots `+0x90` (completion callback dispatch) and `+0x88`
|
||||
(response ownership transfer). All four calls completed on native thread 656:
|
||||
|
||||
```text
|
||||
request = 0xb80cdfe0
|
||||
factory response = 0x7c94808
|
||||
deserializer this = 0x7c94808, result=true
|
||||
+0x90 callback argument = 0x7c94808
|
||||
+0x88 owner-slot address = 0xbc51f7e8
|
||||
```
|
||||
|
||||
The matching `GET /ut/game/fifa17/sbs/sets` at `11:24:00` returned HTTP 200, and the same
|
||||
communication modal appeared. Both callback probes reported `entry=1 exit=1`; no degraded
|
||||
hook state or process failure occurred.
|
||||
|
||||
This proves that the parsed response reaches the category request's completion dispatcher
|
||||
and that its ownership-transfer routine also returns normally. The remaining failure
|
||||
boundary begins at the receiving owner object's vtable `+0x18` consumer invoked from
|
||||
`0x1801631e0`, or later collection/cache/UI validation. Network transport, response
|
||||
construction, native parsing, callback dispatch, and request-side ownership handoff are no
|
||||
longer candidate root causes.
|
||||
@@ -0,0 +1,337 @@
|
||||
# SBC render intervention — injected-DLL integration spec
|
||||
|
||||
**Goal:** make the FIFA 17 FUT **SBC menu render real SBC data** from inside the
|
||||
process (client-side), proven not server-fixable. The DLL is the existing
|
||||
`openfut-hook` (`version.dll`, cross-compiled `x86_64-pc-windows-gnu`, feature
|
||||
`fifa17`). In-process calls to client functions are safe here (unlike `/proc/mem`
|
||||
writes), because we run on the game's own threads with the real allocator.
|
||||
|
||||
**Binary of record (clean-room):** `/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll`
|
||||
(on-disk copy `/tmp/fut/cardsdll.dll`), PE image base `0x180000000`. Every address
|
||||
below was re-verified byte-exact against this PE in this pass (vtable slots read from
|
||||
`.rdata`, prologues from `.text`). Do **not** build/deploy from this spec without the
|
||||
staged morning test (§9).
|
||||
|
||||
---
|
||||
|
||||
## 1. Module base + RVA math
|
||||
|
||||
CardsDLL is **not** present at `DllMain`/worker time — the boot module dump
|
||||
(`C:\openfut_hook.log`) has no `CardsDLL*` entry. It is loaded lazily **only when the
|
||||
user first enters Ultimate Team**. Therefore the hook must **defer** and poll for it,
|
||||
exactly like `probe::install_probes_deferred` polls for `anadius64.dll`.
|
||||
|
||||
- Loaded module name (Wine keeps the on-disk filename): **`CardsDLL_Win64_retail.dll`**.
|
||||
`GetModuleHandleA(b"CardsDLL_Win64_retail.dll\0")`. Fallback: ToolHelp module walk
|
||||
matching a name containing `CardsDLL` (see `fifa17::dump_modules` for the pattern).
|
||||
- Image base in the PE is `0x180000000`. For any static VA in this doc:
|
||||
|
||||
```
|
||||
rva = VA_static - 0x180000000
|
||||
VA_runtime = cards_base + rva
|
||||
```
|
||||
|
||||
`cards_base` is the runtime `HMODULE` of `CardsDLL_Win64_retail.dll` (its in-memory
|
||||
load address). All the "0x180…" addresses below are **static VAs**; subtract
|
||||
`0x180000000` to get the RVA, add `cards_base` to get the live pointer/callable.
|
||||
|
||||
- Slide-proof control (optional sanity, mirrors `tools/gate_byte_probe.py`): the FNV
|
||||
prologue at VA `0x180180d00` must match the on-disk PE bytes
|
||||
`48 83 ec 28 48 85 c9 74 50 45 33 c0 ba c5 9d 1c 81 …`. If it does not, **abort** —
|
||||
the module map moved and the offsets are untrustworthy.
|
||||
|
||||
---
|
||||
|
||||
## 2. Verified object graph
|
||||
|
||||
```
|
||||
A = FUT root singleton = *(0x1802e6398) getter thunk 0x18011a830 = { mov rax,[rip→0x1802e6398]; ret }
|
||||
A.vtable (live [A]) = static 0x18021c2a0
|
||||
A.vtable[+0x4e8] = 0x18011c1f0 = { lea rax,[rcx+0x1f9d8]; ret } -> B getter
|
||||
A.vtable[+0x9b0] = 0x18011b7d0 = M lazy getter (see §3) -> M getter
|
||||
A.vtable[+0x18] = 0x180113f50 = service-id 0xed84b12 -> returns `this` (proves manager == A)
|
||||
|
||||
B = SBC request/ready TTL cache = A + 0x1f9d8 vtable static 0x1801fae70
|
||||
B+0x08 collection ptr (live 0 offline)
|
||||
B+0x20 QPC deadline
|
||||
B+0x28 ready byte (== A+0x1fa00 alias) <- the isValid gate byte
|
||||
B.vtable[+0x00] dtor = 0x180063040
|
||||
B.vtable[+0x08] isValid = 0x180065d40 (see §4)
|
||||
B.vtable[+0x10] clear = 0x180065d20
|
||||
|
||||
M = SBC categories/sets store = *(A + 0x20a68) <- THE RENDER SOURCE (see §3, §5)
|
||||
M+0x50 WORD category count
|
||||
M+0x58 cat-vector begin (element stride 0xf0)
|
||||
M+0x60 cat-vector end
|
||||
M+0xa10 secondary/featured vec begin (8-byte elems) (emptiness-checked at render)
|
||||
M+0xa18 secondary vec end
|
||||
per category (+0xf0 stride):
|
||||
cat+0xb8 WORD set count
|
||||
cat+0xc0 set-vector begin (element stride 0x3570)
|
||||
set+0x1c9 byte per-set flag
|
||||
```
|
||||
|
||||
HUB cache (works online) is the **same class** at `A + 0x1fd70` (vtable `0x18021c1e0`)
|
||||
— reference only.
|
||||
|
||||
**Manager fetch used by BOTH the deser and the render controller** (so
|
||||
populate-target == render-source):
|
||||
|
||||
```
|
||||
reg = 0x1800d7170() ; -> ®istry (static 0x1802c2988)
|
||||
mgr = 0x180009c80(&out, reg) ; out = manager (hashes 0xed84b11 / 0xed84b12)
|
||||
M = mgr.vtable[+0x9b0](mgr) ; 0x18011b7d0, lazily creates/returns *(A+0x20a68)
|
||||
```
|
||||
|
||||
Because svc-id `0xed84b12` resolves to `A` (A.vtable[+0x18] returns `this`),
|
||||
`mgr == A` and `mgr.vtable[+0x9b0] == A.vtable[+0x9b0] == 0x18011b7d0`. The hook may
|
||||
therefore fetch M the short way — `A = *(0x1802e6398); M = (*(void***)A)[0x9b0/8](A)` —
|
||||
**or** the long way (registry) — they return the identical object.
|
||||
|
||||
---
|
||||
|
||||
## 3. M lazy getter — 0x18011b7d0 (verified disassembly)
|
||||
|
||||
```
|
||||
18011b7d0 push rbx; push rdi; sub rsp,0x38
|
||||
18011b7e0 mov rdi,rcx ; rcx = A (this)
|
||||
18011b7e3 cmp QWORD [rcx+0x20a68],0 ; M already built?
|
||||
18011b7eb jne 18011b873 ; yes -> return it
|
||||
18011b7f1 call 0x18019e3c0 ; factory: allocate an EMPTY M (type-id 0x13f0)
|
||||
… … ; init fields, cache at A+0x20a68, return
|
||||
```
|
||||
|
||||
Cold-calling this alone **creates an EMPTY M** (`WORD[M+0x50]==0`) → the menu draws
|
||||
**2 placeholder tiles** (count+2). It does **not** populate. Populating is §5.
|
||||
|
||||
---
|
||||
|
||||
## 4. The gate — isValid 0x180065d40 (verified disassembly)
|
||||
|
||||
```
|
||||
180065d40 push rbx; sub rsp,0x20; mov rbx,rcx ; rcx = B
|
||||
180065d49 call 0x1801642c0 ; online sub-check — STUBBED `mov al,1;ret`
|
||||
180065d4e test al,al ; je fail ; never the wall
|
||||
180065d52 cmp BYTE [rbx+0x28],0 ; je fail ; <-- READY BYTE gate
|
||||
180065d58 cmp QWORD [rbx+0x8],0 ; je 0x180065d75 ; <-- if collection==0 -> RETURN 1 (short-circuit)
|
||||
180065d5f lea rcx,[rsp+0x38]; call [rip→0x1801e50c0]; QueryPerformanceCounter
|
||||
180065d6a mov rax,[rbx+0x20]; sub rax,[rsp+0x38] ; deadline - now
|
||||
180065d73 js fail ; past deadline -> fail
|
||||
180065d75 mov al,1 ; …; ret ; success
|
||||
```
|
||||
|
||||
**Load-bearing correction (adversarially confirmed, verified in this pass):** arm
|
||||
**only** `BYTE[B+0x28]=1` and **leave `QWORD[B+0x08]=0`**. With `B+0x08==0` the function
|
||||
takes the `je 0x180065d75` short-circuit and returns 1 immediately. If you instead
|
||||
write `B+0x08` (pointing it at the collection), isValid falls into the QPC-deadline
|
||||
branch; with the live-stale deadline (`B+0x20 = 0xf10fb8cb9`, already in the past) it
|
||||
returns **0 → modal → gate SHUTS**. So **never** manually write `B+0x08` or `B+0x20`.
|
||||
Rendering reads **M** (§5), not `B+0x08`, so nothing needs `B+0x08` set.
|
||||
|
||||
---
|
||||
|
||||
## 5. Render source — M, not B (verified disassembly)
|
||||
|
||||
Controller ctor caches M into `controller+0x140`:
|
||||
|
||||
```
|
||||
1800b554d call 0x1800d7170 ; reg
|
||||
1800b555d call 0x180009c80 ; mgr = out
|
||||
1800b556b mov rax,[rbx] ; mgr.vtable
|
||||
1800b5571 call [rax+0x9b0] ; M = 0x18011b7d0(mgr)
|
||||
1800b5577 mov [rsi+0x140], rax ; controller+0x140 = M
|
||||
…then registers Scaleform events 0x756c-0x7574 via 0x1801a4a70
|
||||
```
|
||||
|
||||
Tile-count emit (each menu build):
|
||||
|
||||
```
|
||||
1800b5eda mov rax,[r13+0x140] ; rax = M
|
||||
1800b5ee1 movzx ebx,WORD [rax+0x50] ; ebx = category count
|
||||
1800b5ee5 add bx,0x2 ; +2 placeholder tiles
|
||||
1800b5ee9 mov rax,[r15] ; Scaleform model vtable
|
||||
call [rax+0x58](count) ; push (category_count + 2) list tiles
|
||||
```
|
||||
|
||||
Helper thunks (verified): `0x18015fff0 = lea rax,[rcx+0x58]` (&M cat-vector),
|
||||
`0x1801607e0 = lea rax,[rcx+0xa10]` (&M secondary vector). **Zero** reads of
|
||||
`B`/`A+0x1f9d8`/`A+0x1fa00` exist in the tile-build region — B is purely the entry
|
||||
gate. Populate M ⇒ tiles appear.
|
||||
|
||||
---
|
||||
|
||||
## 6. Populate path — reuse the real parser (deser 0x18017b2b0)
|
||||
|
||||
The category rows are appended **only** by the sbs/sets deserializer. Its geometry and
|
||||
finalizers are the correct way to fill M (hand-building `0xf0`/`0x3570` structs is
|
||||
brittle and rejected — §8).
|
||||
|
||||
```
|
||||
18017b2b0 (rcx = this, IGNORED) (rdx = a PRIMED SAX reader over the token stream)
|
||||
18017b2ef mov rdi,rdx ; keeps the incoming reader in rdi (the byte source)
|
||||
18017b2fb call 0x1801c63e0(&localctx, 0, 0) ; builds a SECONDARY ctx with a NULL source
|
||||
18017b309 call 0x1800d7170 ; reg
|
||||
18017b316 call 0x180009c80 ; mgr
|
||||
18017b327 call [mgr.vtable+0x9b0] ; M (0x18011b7d0)
|
||||
… clear 0x18015f3a0(M) ; ALWAYS clears M first (see crash risk C1)
|
||||
… loop atom 0x6f "categories":
|
||||
0x180159da0(&tmp) ; cat ctor (0xf0, vtable 0x18021b520)
|
||||
0x18017ab80(&tmp, reader) ; cat deser (needs the reader)
|
||||
0x180160e50(&tmp) ; cat finalize (set index)
|
||||
0x18015a770(M, &tmp) ; APPEND (copy-in; copy-ctor 0x18015a2b0)
|
||||
0x1801105d0(&tmp) ; cat dtor
|
||||
… 0x180160e00(M); 0x180160f30(M); 0x180161020(M) ; rebuild M indices (+0x9e0/+0xa10/+0xa40)
|
||||
… commit mgr.vtable[+0x8](mgr)
|
||||
18017b751 ret (always true)
|
||||
```
|
||||
|
||||
**The reader (`rdx`) is the crux.** The deser does **not** ingest `rdx` through the
|
||||
`0x1801c63e0` ctx it builds (that one is created with a NULL source, `rdx=0/r8=0`);
|
||||
instead it keeps the **incoming** `rdx` in `rdi` and scans its bytes directly (e.g. the
|
||||
NUL-terminated backslash-unescape at `~0x18017b353` does `mov rdi,[rdi]`). So `rdx`
|
||||
must be a **fully-constructed, already-primed SAX reader/cursor object** seated over
|
||||
your canned `sbs/sets` JSON — the same object type the message framework produces on a
|
||||
real response. **Building that reader from scratch is the one remaining un-reversed
|
||||
contract** (its vtable, and specifically the `[+0x8]` byte-yield slot, are not yet
|
||||
pinned). Until it is, the fully-offline parser-reuse call is **not turnkey** — see the
|
||||
three tiers in §7.
|
||||
|
||||
SAX primitives already known (for when the reader is reconstructed): ctx init
|
||||
`0x1801c63e0(rcx=ctx,rdx=source,r8=flags)`, lexer `0x1801c8060`, next-token
|
||||
`0x1801c7f10`, begin-object `0x1801c8270`, INT `0x1801c79d0`, STR `0x1801c7aa0`,
|
||||
BOOL `0x1801c7620`, SKIP `0x180135ff0`.
|
||||
|
||||
Response-msg object (for the message-layer tier): ctor `0x18017b1c0` installs vtable
|
||||
`0x18022e598`; slot `[+0x20] == 0x18017b2b0` (deser) — **verified**. Constructing this
|
||||
object alone still does **not** seat the reader (the framework does that from received
|
||||
bytes), so it doesn't remove the reader gap.
|
||||
|
||||
---
|
||||
|
||||
## 7. Three intervention tiers (implement in this order)
|
||||
|
||||
**Tier 0 — arm-only negative control (SAFE, non-crash, renders EMPTY).**
|
||||
Resolve A→B, write `BYTE[B+0x28]=1`, leave `B+0x08=0`. isValid short-circuits true, the
|
||||
menu opens and draws **2 placeholder tiles** (M empty/null). Proves the gate model live
|
||||
without any populate. This is the first morning step and the baseline. Implemented and
|
||||
env-gated in `sbc_hook.rs` (`OPENFUT_SBC_ARM_ONLY=1`).
|
||||
|
||||
**Tier 1 — parser-reuse populate (the intended fix, BLOCKED on the reader).**
|
||||
On the game thread: build a primed SAX reader over canned `sbs/sets` JSON served by the
|
||||
bridge/core, `call 0x18017b2b0(rcx=0, rdx=reader)` (self-locates mgr, clears, appends,
|
||||
finalizes, commits → fills M), then Tier-0 arm (`BYTE[B+0x28]=1` only), then trigger a
|
||||
menu refresh (§ below). **Cannot be enabled** until the reader contract (§6) is
|
||||
reversed. `sbc_hook.rs` contains the guarded scaffold that logs the blocker and returns
|
||||
— it does **not** call the deser with a fabricated reader (that would clear M and/or
|
||||
crash — C1/C6).
|
||||
|
||||
**Tier 2 — message-layer injection (cleanest long-term, feasibility unproven).**
|
||||
Push a canned `sbs/sets` response through the real receive path so the framework builds
|
||||
the response-msg (`0x18017b1c0`), seats the reader itself, runs `0x18017b2b0`, fires the
|
||||
completion callback (`0x1800b8c30`, subscribed in svc ctor `0x1800b5765` via
|
||||
`mgr.vtable[+0xa90]`), and arms B natively (generic copy-assign `0x1800c21a0`) — **zero
|
||||
forged state**. Requires reconstructing the message-receive entry + response-msg wiring;
|
||||
treat as the target, not the default.
|
||||
|
||||
**Refresh trigger** (Tier 1/2): the controller re-reads `WORD[M+0x50]` at `0x1800b5eda`
|
||||
on every build, so **re-opening the SBC menu** suffices. Programmatic alternative: fire
|
||||
Scaleform refresh events `0x756c-0x7574` via `0x1801a4a70`. If M is populated but no
|
||||
refresh fires and the controller already cached an empty M at `ctrl+0x140`, you still see
|
||||
2 placeholder tiles (no crash, just no data) — see C7.
|
||||
|
||||
---
|
||||
|
||||
## 8. Function signatures (Win64 `extern "system"`; rcx, rdx, r8, r9 → rax)
|
||||
|
||||
| Purpose | Static VA | Signature (Rust `unsafe extern "system"`) |
|
||||
|---|---|---|
|
||||
| A getter thunk | 0x18011a830 | `fn() -> *mut u8` (returns `*(0x1802e6398)`) |
|
||||
| B getter (via A vtable +0x4e8) | 0x18011c1f0 | `fn(a: *mut u8) -> *mut u8` (`a+0x1f9d8`) |
|
||||
| M lazy getter (A vtable +0x9b0) | 0x18011b7d0 | `fn(mgr: *mut u8) -> *mut u8` (`*(mgr+0x20a68)`, lazily built) |
|
||||
| isValid (B vtable +0x08) | 0x180065d40 | `fn(b: *mut u8) -> bool` |
|
||||
| registry getter | 0x1800d7170 | `fn() -> *mut u8` |
|
||||
| manager getter | 0x180009c80 | `fn(out: *mut *mut u8, reg: *mut u8) -> *mut u8` |
|
||||
| sbs/sets deser (whole) | 0x18017b2b0 | `fn(this_ignored: *mut u8, reader: *mut u8) -> bool` |
|
||||
| SAX ctx init | 0x1801c63e0 | `fn(ctx: *mut u8, source: *mut u8, flags: u64) -> *mut u8` |
|
||||
| clear M | 0x18015f3a0 | `fn(m: *mut u8)` |
|
||||
| cat ctor (0xf0) | 0x180159da0 | `fn(tmp: *mut u8) -> *mut u8` |
|
||||
| cat deser | 0x18017ab80 | `fn(tmp: *mut u8, reader: *mut u8) -> bool` |
|
||||
| cat finalize | 0x180160e50 | `fn(tmp: *mut u8)` |
|
||||
| append into M | 0x18015a770 | `fn(m: *mut u8, tmp: *mut u8)` |
|
||||
| cat dtor | 0x1801105d0 | `fn(tmp: *mut u8)` |
|
||||
| M index rebuild ×3 | 0x180160e00 / 0x180160f30 / 0x180161020 | `fn(m: *mut u8)` each |
|
||||
| QueryPerformanceCounter thunk | 0x1801e50c0 | (indirect; not needed if B+0x08 left 0) |
|
||||
| Scaleform refresh dispatch | 0x1801a4a70 | `fn(ctrl: *mut u8, event_id: u32, …)` (event ids 0x756c-0x7574) |
|
||||
|
||||
M is **per-session heap** — never hardcode its address; always go A → `A.vtable[+0x9b0]`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Staged morning test plan (human, live)
|
||||
|
||||
Preconditions: FIFA 17 at the FUT hub (so CardsDLL is loaded). One env var flips each
|
||||
tier; all default **off/inert**. Watch `C:\openfut_hook.log`.
|
||||
|
||||
1. **Injection + resolution (read-only).** Launch with `OPENFUT_SBC_HOOK=1` only. The
|
||||
deferred thread should log: CardsDLL base + slide-control OK, then `A=…`, `B=…`,
|
||||
`B+0x28=0`, `M=*(A+0x20a68)=…` (0 until the SBC menu is opened once). No writes.
|
||||
*Pass:* addresses match the model; control FNV OK.
|
||||
2. **Tier-0 arm-only (negative control).** Add `OPENFUT_SBC_ARM_ONLY=1`. Open the SBC
|
||||
menu. Expected: **menu opens, draws ~2 empty placeholder tiles, no modal, no crash.**
|
||||
Confirms the gate byte and short-circuit live. If it crashes → stop (means B
|
||||
resolution is wrong; recheck slide).
|
||||
3. **Tier-1 populate — BLOCKED.** Do **not** enable until the SAX reader contract (§6)
|
||||
is reversed. `OPENFUT_SBC_POPULATE=1` currently only logs the blocker and returns.
|
||||
Next RE session: pin the reader vtable (`[+0x8]` byte-yield) and the reader ctor,
|
||||
then wire the §6 sequence and re-test on the game thread with the menu **closed**,
|
||||
then re-open to refresh.
|
||||
4. Revert env vars to unset when done.
|
||||
|
||||
---
|
||||
|
||||
## 10. Crash-risk register (verified against the PE + prior adversarial passes)
|
||||
|
||||
- **C1 — cold-calling deser without a real reader.** `0x18017b2b0` **clears M first**
|
||||
(`0x18015f3a0` before any append). A null/garbage reader → parses nothing but **wipes
|
||||
M** (renders empty, destroys prior state), and the byte-scan at `~0x18017b353`
|
||||
(`mov rdi,[rdi]`) segfaults on a bad pointer. This is exactly why Tier 1 is gated off.
|
||||
- **C2 — clear/finalize race.** Deser clears then rebuilds M's vectors+indices; if the
|
||||
render thread reads `WORD[M+0x50]` (`0x1800b5eda`) or by-index `0x180160a80` mid-build
|
||||
→ OOB/crash. Populate on the game thread with the menu **closed**, then refresh.
|
||||
- **C3 — skipping finalizers.** Any manual append via `0x18015a770` **must** be followed
|
||||
by `0x180160e00`/`0x180160f30`/`0x180161020` or the `+0x9e0/+0xa10/+0xa40` indices go
|
||||
stale and by-index lookups read OOB.
|
||||
- **C4 — hand-built `0xf0`/`0x3570` structs.** Append's copy-ctor `0x18015a2b0`
|
||||
deep-copies EASTL sub-vectors; a bad begin/end/cap → heap corruption. **Rejected**
|
||||
(§8): drive the real parser instead.
|
||||
- **C5 — writing `B+0x08`/`B+0x20`.** Forces isValid into the deadline branch; the
|
||||
live-stale deadline shuts the gate → modal. **Set only `B+0x28`, leave `B+0x08=0`.**
|
||||
- **C6 — null manager/M.** Deser does `mov rax,[mgr]`; if the registry lookup returned
|
||||
null it's a null-deref. Live registry `*(0x1802c2988)` is non-null offline, but the
|
||||
hook must null-check A, mgr, M before any use.
|
||||
- **C7 — no refresh (non-crash).** Populate without firing refresh / re-open → controller
|
||||
keeps its cached empty M → still 2 placeholder tiles. Fails the goal, not a crash.
|
||||
- **C8 — foreign-thread allocation.** The lazy getter and appenders allocate on / mutate
|
||||
the game heap; running them off the main/render thread races the allocator. Execute the
|
||||
populate on a game thread (message-pump / a game-thread detour), not a bg thread. The
|
||||
Tier-0 single-byte arm is tolerant of a bg write (it's what the `/proc` poke does), but
|
||||
populate is not.
|
||||
|
||||
---
|
||||
|
||||
## 11. Live-probe baseline (this pass, read-only `O_RDONLY`, zero writes)
|
||||
|
||||
FIFA17.exe **was running** at spec time (pid 12201), CardsDLL mapped. Fresh live reads
|
||||
this pass match the static model 1:1:
|
||||
|
||||
```
|
||||
slide 0x6ffe7c140000 CONTROL FNV OK
|
||||
A 0xb83e2b60 (= *(0x1802e6398))
|
||||
B 0xb8402538 vt=0x1801fae70 (matches static) B+0x08(coll)=0 B+0x20=0xf10fb8cb9 B+0x28(ready)=0
|
||||
HUB 0xb84028d0 vt=0x18021c1e0 coll=0 ready=0 (reference only)
|
||||
M *(A+0x20a68)=0 (SBC menu not opened this session -> M not yet built)
|
||||
```
|
||||
|
||||
So live: gate SHUT (`B+0x28=0`), collection null, **M null** — Tier-0 arm alone would
|
||||
render empty (matches the model). All §2–§6 addresses + all vtable slots were
|
||||
re-verified byte-exact against the on-disk PE in this pass.
|
||||
Executable
+252
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env bash
|
||||
# FIFA 17 hook M1 staging/deployment helper.
|
||||
#
|
||||
# Safe defaults:
|
||||
# inspect (the default) is read-only;
|
||||
# stage writes only below the repository;
|
||||
# deploy and launch require separate, exact confirmation variables.
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
hook_root="${repo_root}/openfut-launcher/openfut-hook"
|
||||
default_dll="${hook_root}/target/x86_64-pc-windows-gnu/release/openfut_hook.dll"
|
||||
stage_root="${repo_root}/fifa17-recon/staging/fifa17-hook-m1"
|
||||
|
||||
game_dir="${OPENFUT_FIFA17_GAME_DIR:-/mnt/games/FIFA 17}"
|
||||
wine_prefix="${OPENFUT_FIFA17_WINEPREFIX:-/home/alex/Games/umu/fifa17}"
|
||||
proton_path="${OPENFUT_FIFA17_PROTONPATH:-UMU-Proton-10.0-4}"
|
||||
hook_dll="${OPENFUT_FIFA17_HOOK_DLL:-${default_dll}}"
|
||||
system_version="${wine_prefix}/drive_c/windows/system32/version.dll"
|
||||
deployed_dll="${game_dir}/version.dll"
|
||||
|
||||
required_exports=(
|
||||
GetFileVersionInfoA GetFileVersionInfoExA GetFileVersionInfoExW
|
||||
GetFileVersionInfoSizeA GetFileVersionInfoSizeExA GetFileVersionInfoSizeExW
|
||||
GetFileVersionInfoSizeW GetFileVersionInfoW VerFindFileA VerFindFileW
|
||||
VerInstallFileA VerInstallFileW VerLanguageNameA VerLanguageNameW
|
||||
VerQueryValueA VerQueryValueW
|
||||
)
|
||||
|
||||
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
|
||||
note() { printf '%s\n' "$*"; }
|
||||
need_file() { [[ -f "$1" ]] || die "missing file: $1"; }
|
||||
|
||||
sha256() { sha256sum -- "$1" | awk '{print $1}'; }
|
||||
|
||||
pe_exports() {
|
||||
x86_64-w64-mingw32-objdump -p "$1" |
|
||||
awk '/\[Ordinal\/Name Pointer\] Table/{in_names=1; next} in_names && /\+base\[/ {print $NF}'
|
||||
}
|
||||
|
||||
verify_pe64() {
|
||||
local dll=$1
|
||||
local format
|
||||
format="$(x86_64-w64-mingw32-objdump -f "$dll" | awk '/file format/{print $NF}')"
|
||||
[[ "$format" == "pei-x86-64" ]] || die "$dll is not a 64-bit PE DLL (format=${format:-unknown})"
|
||||
}
|
||||
|
||||
verify_exports() {
|
||||
local dll=$1 export_name
|
||||
local exports
|
||||
exports="$(pe_exports "$dll")"
|
||||
for export_name in "${required_exports[@]}"; do
|
||||
grep -Fxq "$export_name" <<<"$exports" ||
|
||||
die "$dll lacks VERSION export $export_name; refusing to stage/deploy"
|
||||
done
|
||||
}
|
||||
|
||||
verify_inputs() {
|
||||
command -v sha256sum >/dev/null || die "sha256sum is required"
|
||||
command -v x86_64-w64-mingw32-objdump >/dev/null ||
|
||||
die "x86_64-w64-mingw32-objdump is required"
|
||||
need_file "$hook_dll"
|
||||
need_file "$system_version"
|
||||
verify_pe64 "$hook_dll"
|
||||
}
|
||||
|
||||
inspect() {
|
||||
verify_inputs
|
||||
note "mode=inspect (read-only)"
|
||||
note "hook=$hook_dll"
|
||||
note "hook_sha256=$(sha256 "$hook_dll")"
|
||||
note "system_version=$system_version"
|
||||
note "system_version_sha256=$(sha256 "$system_version")"
|
||||
note "game_dir=$game_dir"
|
||||
if [[ -f "$deployed_dll" ]]; then
|
||||
note "deployed_version_sha256=$(sha256 "$deployed_dll")"
|
||||
else
|
||||
note "deployed_version=absent"
|
||||
fi
|
||||
verify_exports "$hook_dll"
|
||||
note "version_exports=complete"
|
||||
}
|
||||
|
||||
build() {
|
||||
command -v cargo >/dev/null || die "cargo is required"
|
||||
note "Building the inert FIFA 17 hook into the package-local staging source path."
|
||||
CARGO_TARGET_DIR="${hook_root}/target" \
|
||||
cargo build --offline --release --features fifa17 \
|
||||
--target x86_64-pc-windows-gnu --manifest-path "${hook_root}/Cargo.toml"
|
||||
inspect
|
||||
}
|
||||
|
||||
stage() {
|
||||
verify_inputs
|
||||
verify_exports "$hook_dll"
|
||||
need_file "${game_dir}/CardsDLL_Win64_retail.dll"
|
||||
need_file "${game_dir}/FIFA17.exe"
|
||||
mkdir -p "$stage_root"
|
||||
local staged="${stage_root}/version.dll"
|
||||
cp -- "$hook_dll" "$staged"
|
||||
{
|
||||
printf 'artifact=%s\n' "$staged"
|
||||
printf 'artifact_sha256=%s\n' "$(sha256 "$staged")"
|
||||
printf 'source=%s\n' "$hook_dll"
|
||||
printf 'source_sha256=%s\n' "$(sha256 "$hook_dll")"
|
||||
printf 'system_version=%s\n' "$system_version"
|
||||
printf 'system_version_sha256=%s\n' "$(sha256 "$system_version")"
|
||||
printf 'cards_dll_sha256=%s\n' "$(sha256 "${game_dir}/CardsDLL_Win64_retail.dll")"
|
||||
printf 'fifa17_exe_sha256=%s\n' "$(sha256 "${game_dir}/FIFA17.exe")"
|
||||
} >"${stage_root}/manifest.txt"
|
||||
note "staged=$staged"
|
||||
note "manifest=${stage_root}/manifest.txt"
|
||||
note "No game-directory file was changed."
|
||||
}
|
||||
|
||||
require_game_stopped() {
|
||||
if pgrep -fi '(FIFA17|_fifa17)\.exe' >/dev/null; then
|
||||
die "FIFA 17 appears to be running; close it before deployment"
|
||||
fi
|
||||
}
|
||||
|
||||
deploy() {
|
||||
[[ "${OPENFUT_FIFA17_DEPLOY:-}" == "I_ACCEPT_VERSION_DLL_REPLACEMENT" ]] ||
|
||||
die "deploy requires OPENFUT_FIFA17_DEPLOY=I_ACCEPT_VERSION_DLL_REPLACEMENT"
|
||||
require_game_stopped
|
||||
local staged="${stage_root}/version.dll"
|
||||
local manifest="${stage_root}/manifest.txt"
|
||||
need_file "$staged"
|
||||
need_file "$manifest"
|
||||
verify_pe64 "$staged"
|
||||
verify_exports "$staged"
|
||||
local recorded actual
|
||||
recorded="$(awk -F= '$1=="artifact_sha256"{print $2}' "$manifest")"
|
||||
actual="$(sha256 "$staged")"
|
||||
[[ -n "$recorded" && "$recorded" == "$actual" ]] || die "staged artifact hash does not match manifest"
|
||||
|
||||
local backup_dir="${game_dir}/openfut-backups"
|
||||
mkdir -p "$backup_dir"
|
||||
if [[ -f "$deployed_dll" ]]; then
|
||||
local old_hash backup
|
||||
old_hash="$(sha256 "$deployed_dll")"
|
||||
backup="${backup_dir}/version.dll.${old_hash}.bak"
|
||||
if [[ ! -e "$backup" ]]; then
|
||||
cp -- "$deployed_dll" "$backup"
|
||||
fi
|
||||
[[ "$(sha256 "$backup")" == "$old_hash" ]] || die "backup verification failed: $backup"
|
||||
note "backup=$backup"
|
||||
fi
|
||||
cp -- "$staged" "$deployed_dll"
|
||||
[[ "$(sha256 "$deployed_dll")" == "$actual" ]] || die "deployed DLL hash verification failed"
|
||||
note "deployed=$deployed_dll"
|
||||
note "deployed_sha256=$actual"
|
||||
}
|
||||
|
||||
launch() {
|
||||
local mode=${1:-baseline}
|
||||
local hook_enabled=0
|
||||
local trace_enabled=0
|
||||
local request_trace_enabled=0
|
||||
local notifier_trace_enabled=0
|
||||
case "$mode" in
|
||||
baseline)
|
||||
[[ "${OPENFUT_FIFA17_LAUNCH:-}" == "I_ACCEPT_M1_BASELINE_LAUNCH" ]] ||
|
||||
die "launch requires OPENFUT_FIFA17_LAUNCH=I_ACCEPT_M1_BASELINE_LAUNCH"
|
||||
;;
|
||||
resolve)
|
||||
[[ "${OPENFUT_FIFA17_RESOLVE:-}" == "I_ACCEPT_M2_RESOLVE_LAUNCH" ]] ||
|
||||
die "launch-resolve requires OPENFUT_FIFA17_RESOLVE=I_ACCEPT_M2_RESOLVE_LAUNCH"
|
||||
hook_enabled=1
|
||||
;;
|
||||
trace)
|
||||
[[ "${OPENFUT_FIFA17_TRACE:-}" == "I_ACCEPT_M3_PASSIVE_TRACE" ]] ||
|
||||
die "launch-trace requires OPENFUT_FIFA17_TRACE=I_ACCEPT_M3_PASSIVE_TRACE"
|
||||
hook_enabled=1
|
||||
trace_enabled=1
|
||||
request_trace_enabled=1
|
||||
notifier_trace_enabled=1
|
||||
;;
|
||||
*) die "unknown launch mode: $mode" ;;
|
||||
esac
|
||||
need_file "$deployed_dll"
|
||||
local staged="${stage_root}/version.dll"
|
||||
local manifest="${stage_root}/manifest.txt"
|
||||
need_file "$staged"
|
||||
need_file "$manifest"
|
||||
verify_pe64 "$deployed_dll"
|
||||
verify_exports "$deployed_dll"
|
||||
local recorded
|
||||
recorded="$(awk -F= '$1=="artifact_sha256"{print $2}' "$manifest")"
|
||||
[[ -n "$recorded" && "$(sha256 "$staged")" == "$recorded" ]] ||
|
||||
die "staged artifact hash does not match manifest"
|
||||
[[ "$(sha256 "$deployed_dll")" == "$recorded" ]] ||
|
||||
die "deployed version.dll does not match the staged M1 artifact"
|
||||
command -v umu-run >/dev/null || die "umu-run is required"
|
||||
for name in OPENFUT_SBC_DISPATCH OPENFUT_SBC_COMMIT OPENFUT_SBC_ARM_ONLY OPENFUT_SBC_POPULATE; do
|
||||
[[ -z "${!name:-}" || "${!name}" == "0" ]] || die "$name must be unset or 0 for this launch"
|
||||
done
|
||||
mkdir -p "${wine_prefix}/dosdevices"
|
||||
ln -sfn /mnt "${wine_prefix}/dosdevices/w:"
|
||||
note "Launching $mode mode (SBC_HOOK=$hook_enabled; SBC_TRACE=$trace_enabled; SBC_REQUEST_TRACE=$request_trace_enabled; SBC_NOTIFIER_TRACE=$notifier_trace_enabled; every mutation feature disabled); log=/tmp/fifa17-hook-m1-launch.log"
|
||||
cd "$game_dir"
|
||||
env \
|
||||
GAMEID=fifa17 \
|
||||
PROTONPATH="$proton_path" \
|
||||
WINEPREFIX="$wine_prefix" \
|
||||
WINEDLLOVERRIDES='version=n,b' \
|
||||
OPENFUT_SBC_HOOK="$hook_enabled" \
|
||||
OPENFUT_SBC_TRACE="$trace_enabled" \
|
||||
OPENFUT_SBC_REQUEST_TRACE="$request_trace_enabled" \
|
||||
OPENFUT_SBC_NOTIFIER_TRACE="$notifier_trace_enabled" \
|
||||
OPENFUT_SBC_DISPATCH=0 \
|
||||
OPENFUT_SBC_COMMIT=0 \
|
||||
OPENFUT_SBC_ARM_ONLY=0 \
|
||||
OPENFUT_SBC_POPULATE=0 \
|
||||
umu-run _fifa17.exe 2>&1 | tee /tmp/fifa17-hook-m1-launch.log
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: fifa17-hook-m1.sh [inspect|build|stage|deploy|launch|launch-resolve|launch-trace]
|
||||
|
||||
inspect Read-only PE/hash/export preflight (default).
|
||||
build Cross-build the inert FIFA17 hook, then run inspect.
|
||||
stage Copy a verified DLL into repo-local staging and write a hash manifest.
|
||||
deploy Back up and install version.dll; requires:
|
||||
OPENFUT_FIFA17_DEPLOY=I_ACCEPT_VERSION_DLL_REPLACEMENT
|
||||
launch Start the M1 inert-hook baseline; requires:
|
||||
OPENFUT_FIFA17_LAUNCH=I_ACCEPT_M1_BASELINE_LAUNCH
|
||||
launch-resolve
|
||||
Start M2 resolve-only mode (guarded reads/logging, no detours/writes); requires:
|
||||
OPENFUT_FIFA17_RESOLVE=I_ACCEPT_M2_RESOLVE_LAUNCH
|
||||
launch-trace
|
||||
Start the single M3 passive factory/deserializer trace; requires:
|
||||
OPENFUT_FIFA17_TRACE=I_ACCEPT_M3_PASSIVE_TRACE
|
||||
|
||||
Optional path overrides:
|
||||
OPENFUT_FIFA17_HOOK_DLL, OPENFUT_FIFA17_GAME_DIR,
|
||||
OPENFUT_FIFA17_WINEPREFIX, OPENFUT_FIFA17_PROTONPATH
|
||||
EOF
|
||||
}
|
||||
|
||||
case "${1:-inspect}" in
|
||||
inspect) inspect ;;
|
||||
build) build ;;
|
||||
stage) stage ;;
|
||||
deploy) deploy ;;
|
||||
launch) launch baseline ;;
|
||||
launch-resolve) launch resolve ;;
|
||||
launch-trace) launch trace ;;
|
||||
-h|--help|help) usage ;;
|
||||
*) usage >&2; die "unknown command: $1" ;;
|
||||
esac
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
# Launch x64dbg inside FIFA 17's UMU/Proton prefix.
|
||||
set -euo pipefail
|
||||
|
||||
x64dbg_root="${OPENFUT_X64DBG_ROOT:-/home/alex/Games/x64dbg}"
|
||||
wine_prefix="${OPENFUT_FIFA17_WINEPREFIX:-/home/alex/Games/umu/fifa17}"
|
||||
proton_path="${OPENFUT_FIFA17_PROTONPATH:-UMU-Proton-10.0-4}"
|
||||
debugger="${x64dbg_root}/release/x64/x64dbg-unsigned.exe"
|
||||
|
||||
[[ -f "$debugger" ]] || {
|
||||
printf 'ERROR: x64dbg executable not found: %s\n' "$debugger" >&2
|
||||
exit 1
|
||||
}
|
||||
command -v umu-run >/dev/null || {
|
||||
printf 'ERROR: umu-run is required\n' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cd "${x64dbg_root}/release/x64"
|
||||
exec env \
|
||||
GAMEID=fifa17 \
|
||||
PROTONPATH="$proton_path" \
|
||||
WINEPREFIX="$wine_prefix" \
|
||||
QT_OPENGL=desktop \
|
||||
umu-run "$debugger" "$@"
|
||||
+1
-1
Submodule openfut-launcher updated: 87241acc1a...3d895fb7ac
Reference in New Issue
Block a user