fifa17-recon: match rewards, POW online layer, account backend, quick sell
Second session. FUT core loop, the EASFC/POW online layer, a central account
backend, and a lot of corrections. Everything risky is behind an env flag with
the default set to whatever was live-proven.
WORKING END TO END (live-verified this session):
* match loop -- POST/PUT/POST/DELETE ut/%s/match, rewards via FutDestroyMatch
(0x180121b60). Play a match, get coins, W/D/L updates.
* packs -- buy, cards land in the club, session survives (FUT_PACK_AUTOCLUB=1)
* quick sell -- POST ut/delete/%s/item was UNMAPPED and paid NOTHING; six cards
were destroyed for 0 coins. Now credits discardValue.
* POW/EASFC online -- the "EA FC servers unreachable" banner is powdll's layer,
a THIRD http api on :8094 nobody had served. Redirect needs no root: powdll
FUN_18005a460 reads FIFA_POW_URL from the same client-config store as
ROSTERUPDATE_URL. FUT_POW=1.
* account backend -- fut_account.py replaces 7 hardcoded copies of the persona
across 5 files; club/persona/online-profile editable via CLI.
CORRECTIONS TO ENDPOINT_MAP (all re-extracted from the deserializers):
* FutStoreGetPackTypes: id/packType/isPremium/quantity/saleType/purchaseLimit/
purchaseCount are NOT skipped no-ops -- all are parsed. extPrice inner objects
take externalPriceId(0x11a), not amount/currency.
* FutMoveCard 0x180128600 has NO skip handler (FUN_180135ff0 appears zero times,
unique among FUT deserializers) and parses only itemData -> dreamSquads.
* class -> deserializer resolution: the name literal is preceded by a 4-BYTE
HEADER and the factory LEA points at the header, so look up name_addr - 4.
Six attempts failed on this; now ghidra_env.class_deser(). Unlocked 11 SBC/
Draft schemas.
* live-only endpoints the request table never lists: ut/%s/squad/list,
ut/%s/user/club, ut/%s/club/stats/*, ut/%s/clientdata/<key>. The template
table is a floor, not a ceiling -- the log is the only ground truth.
* 163 RS4 call names exist; we served 17. All now served.
FIXED: club/stats/* was answering with the entire 28-item club inventory on every
poll (it fell through to the generic /club route).
UNSOLVED: the pack reveal's "Send to Club" (PUT ut/%s/item) kills the FUT session
whatever we answer -- {} included -- while its sibling quick-sell endpoint accepts
a bare {}. Seven hypotheses eliminated by live test, documented in
REBUILD_RESEARCH.md S14c so none get re-walked. FUT_PACK_AUTOCLUB routes around it.
Also unfixed: store tiles render "unknown" (displayGroup is parsed RECURSIVELY by
the same element parser; sending it FROZE the store, so FUT_STORE_GROUPS=1 is
default off).
Tests: test_fut_contract.py 380 (live, read-only) + test_match_rewards.py 51 (pure).
Note: fut_store.py carries some pre-existing uncommitted changes from before this
session (pack catalogue ids, pending-pile behaviour) that could not be separated
from this session's additions in the same file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
This commit is contained in:
@@ -634,3 +634,66 @@ Fix: route `/squad/list` (before the generic `/squad`) to `squad_list_body()`:
|
||||
Guarded by a contract check asserting `/squad/list` returns a `squad` array and is
|
||||
*not* the active-squad object. This also makes the `FUT_SQUAD_LIST=merged` workaround
|
||||
(S3) unnecessary — the two responses have distinct URLs, so no merged body is needed.
|
||||
|
||||
## 11. The online gate is POW/EASFC, not FUT (2026-08-03)
|
||||
|
||||
"EA FC servers are unreachable / PRESS Q TO RE-CONNECT" is **not** the FUT/UTAS
|
||||
layer, not Blaze and not Origin/LSX — all three are healthy in our live logs while
|
||||
the banner shows. It is the EASFC layer in **`powdll_Win64_retail.dll`**: 1.1 MB,
|
||||
**unpacked and string-rich**, so unlike Denuvo-packed FIFA17.exe it can actually be
|
||||
reversed. CardsDLL contains zero hits for "unreachable"/"RE-CONNECT"/"PRESS Q";
|
||||
powdll has `TXT_EASFC_RECONNECT_PROMPT`, `TXT_EASFC_SERVER_ERROR`, and the FE events
|
||||
`POWService::PowReconnect` / `TriggerPleaseConnectMsg` / `PowBlazeDisconnected`.
|
||||
|
||||
POW is a **third HTTP API** we have never served:
|
||||
|
||||
| role | default host | paths |
|
||||
|---|---|---|
|
||||
| api | `pas.gt.easfc.ea.com:8094` | `pow/auth`, `pow/healthcheck/system/all`, `pow/v2/activity`, `pow/nucleus/entitlements`, `pow/bank/user/account`, `pow/store/...`, … (58 templates extracted) |
|
||||
| content | `content.lt.easfc.ea.com:8080` | `pow/imgAssets/...`, `pow/artAssets/...` |
|
||||
|
||||
Neither hostname is in `/etc/hosts` or the iptables DNAT, so **every POW call dies at
|
||||
DNS** — which is exactly the banner's trigger.
|
||||
|
||||
### Reversed (Ghidra project `/tmp/pow/powproj`, PE base 0x180000000)
|
||||
|
||||
| addr | role |
|
||||
|---|---|
|
||||
| `FUN_18005a460` | POW config init. Reads `FIFA_POW_URL`, `FIFA_POW_CONTENT_SERVER_URL`, `POW_IS_ON` via `cfg->vtbl[0x30]` = `getString(key, default, &out)` — **the same merged `_all` client-config store that already delivers `ROSTERUPDATE_URL`**. Picks `http://` vs `https://` (`PTR_s_http____18010aee0` / `...aee8`). |
|
||||
| `FUN_18005cb40` | health-check / reconnect handler. Issues `pow/healthcheck/system/all` through request builder `FUN_18005e780`, then sets POW state at `POWmgr[0x6ac]`: **1 = connected, 3 = disconnected** (3 raises the prompt). Also the `PowReconnect` FE-event site. |
|
||||
| `FUN_18005c970` | fires `POWService::PowBlazeDisconnected` |
|
||||
| `FUN_1800a8590` | fires `POWService::TriggerPleaseConnectMsg` |
|
||||
| `FUN_1800ad090` | references `TXT_EASFC_RECONNECT_PROMPT` (the banner) |
|
||||
|
||||
**Consequence: POW can be redirected with no root and no `/etc/hosts`** — just serve
|
||||
`FIFA_POW_URL` from `blaze_responder_v3b.py`.
|
||||
|
||||
### Shipped (all OFF by default)
|
||||
|
||||
* `tools/pow_server.py` — POW/EASFC server on `:8094` (api) + `:8080` (content).
|
||||
`POW_MODE=log` (default) answers everything `200 {}` / assets `404` and logs the
|
||||
exact method+path+headers+body to `/tmp/pow_server.log`; `POW_MODE=serve` adds
|
||||
first-draft bodies for auth/healthcheck/counts. It knows all 58 extracted path
|
||||
templates and **flags any path outside that set**, so the capture also tells us
|
||||
where the extraction was incomplete.
|
||||
* `blaze_responder_v3b.py` — `OSDK_POW` keys, merged onto **every** CFID (same
|
||||
reasoning as `FUT_RS4_*`: which section powdll reads is unproven). Empty unless
|
||||
`FUT_POW=1`.
|
||||
* `openfut-fut.sh` — `pow` added to SERVERS (inert while idle: it only binds ports;
|
||||
nothing points at it until `FUT_POW=1`).
|
||||
* `root_arm.sh pow` / `root_arm.sh unpow` — the `/etc/hosts` fallback, opt-in
|
||||
because those entries persist across reboots.
|
||||
|
||||
### NOT done — the response schemas
|
||||
|
||||
Only the REQUEST side is mapped. No powdll response parser has been walked, so every
|
||||
body `pow_server.py` returns is a placeholder. **The next step is a capture run:**
|
||||
|
||||
```
|
||||
FUT_POW=1 ./openfut-fut.sh restart # then launch FIFA, enter FUT
|
||||
tail -f /tmp/pow_server.log # what does POW actually ask for?
|
||||
```
|
||||
|
||||
Whatever appears there turns the schemas from guesswork into reversing targets —
|
||||
the same route that made the squad work tractable. Instant fallback: drop `FUT_POW`
|
||||
and restart.
|
||||
|
||||
Reference in New Issue
Block a user