09bb2dc30660ff0f5a2df9b35a5f0cdc7e7cf47d
486 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cf3ddde3a6 |
blaze-host: move the dirty-tree safeguard to launch and evidence time
The compiled-in dirty flag cannot be trusted for this job. Cargo does not
re-run a build script when another crate's source changes, so editing the
adapter and rebuilding the host leaves it reading 'clean' -- verified by
appending a line to the adapter and watching the flag not move.
So the stamp now only names the commit, and the real safeguards run at the
moment they matter and cannot go stale:
* sidecar.sh checks the working tree at LAUNCH and warns.
* check-live-parity.sh REFUSES on a dirty tree, since it produces the
artefact a migration decision is made from. ALLOW_DIRTY=1 overrides for a
throwaway check.
Both scope to the three migration crates, so unrelated submodule dirt does not
trigger them -- a warning that is always on is a warning nobody reads.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
468b006008 |
blaze-host: scope the dirty-tree check to the crates the binary is built from
A whole-repo check read DIRTY permanently, because unrelated submodules carry pre-existing modifications. A warning that is always on is a warning nobody reads, which defeats the point: the flag exists so a mutated build announces itself before it can be mistaken for parity evidence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e091921b18 |
blaze-host: safe sidecar lifecycle, Blaze switch, build identity
Prerequisites for the live FIFA A/B. Two safeguards here exist because the corresponding failure actually happened, not because it was imagined. BUILD IDENTITY. build.rs stamps commit + working-tree cleanliness; the host prints commit, tree state, profile and a fingerprint of the bundled config table at startup, into both the log and the trace. A dirty tree prints an explicit "do NOT treat results from this binary as parity evidence" warning. The previous step left four sidecars running, two serving mutated builds, and nothing in their output said so. SIDECAR LIFECYCLE (sidecar.sh). start/stop/status/check-orphans/with. Start refuses when any sidecar is already running or the port is busy. Stop kills, waits, then PROVES it: PID gone AND port free AND no stray processes, failing if any check does not hold. `with -- CMD` traps EXIT/INT/TERM so cleanup runs however the command exits. Bug found and fixed while testing it: orphan detection used `pgrep -f`, which matched any process whose command line merely mentioned the name -- including the shell running the test script. It now matches the resolved executable via /proc/PID/exe. `pgrep -x` is unusable because Linux truncates the process name to "openfut-blaze-h". BLAZE SWITCH (blaze-switch.sh). Redirects Blaze to the sidecar with a scoped NAT rule instead of editing the frozen Python oracle, whose redirector advertises a hardcoded BLAZE_PORT = 42130. Rules match only <LAN_IP>:42130; 127.0.0.1:42130 is deliberately left alone so Python stays reachable on loopback and the A/B compares real Python against real Rust. Verified both directions live: LAN->Rust with the switch on, LAN->Python with it off. Bug found and fixed: `off` reported success while two rules remained active and rollback had NOT happened. It matched `--comment "tag"` with quotes this iptables does not emit -- and the verification used the SAME broken matcher, so it confirmed its own failure. A rollback that lies is worse than one that fails. Now matched on the bare tag, verified with iptables-save plus a tag-independent check that nothing still redirects the port. Second flaw fixed: `sidecar.sh stop` originally warned about a live switch and then stopped anyway, creating the exact broken state it warned about. It now REFUSES, with --force as the deliberate override. The general rule this all converges on, now stated in the README: a verification must not share the failure mode of the thing it verifies. 116 tests still passing; clippy clean; Python backend untouched and contract suite 446/446. NAT table left clean, no orphan processes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a9eb54ae9c |
openfut-blaze-host: thin Blaze sidecar, live-parity with Python
Third migration step, and the one that turns fixture parity into transport parity. A TCP host that frames a Fire2 stream, keeps one Session per connection, calls openfut-adapter-fifa17::dispatch(), and writes the returned frames in order. It owns a socket, a buffer, a session and diagnostics -- that is the complete list. No coins, club, packs, profiles or UTAS logic: those belong to Core, reached through the adapter later. NO TLS, and that is evidence-based rather than an omission. The Blaze main port is plaintext: sending a raw Fire2 Util::ping to the running backend returns a plaintext PingResponse, blaze_handle uses the raw socket, and only redir_handle wraps ssl. TLS belongs to the redirector phase. LIVE A/B AGAINST THE RUNNING PYTHON BACKEND: 101 frames across three conversations, identical normalized traces. This is the first result in the migration that is not purely offline. check-live-parity.sh replays the recorded conversations against both endpoints over real sockets and diffs volatile-masked traces; session keys and clocks are masked, so anything that differs is behavioural. Transport tests cover what fixtures cannot: byte-for-byte replay over a socket, requests dribbled one byte at a time, several requests in one write, the four-frame login burst ordered on the wire, session state persisting across frames and NOT leaking between connections, an absurd payload length closing the connection instead of allocating, and an undecodable body still getting a reply. 18 tests here, 116 across the three migration crates. MUTATION TESTED, including the comparison itself. Dropping a post-login notification is caught by the probe (frame count) AND the diff; a same-length content change deep inside a notification body (CTY "US"->"GB", payload 116 both sides) is caught ONLY by the trace digest. So the probe's exit code is not the test -- the diff is, and the README says so. check-live-parity.sh was itself verified to exit 1 under mutation. The listen port is required configuration with no default, so the sidecar cannot silently collide with the working container. OPENFUT_BIND stays the advertised-config bind (the adapter derives nucleusConnect from it, reproducing the oracle) and the listener gets its own setting, so the two are not conflated. Gates 1-4 pass and are re-runnable. Gates 5-10 need a FIFA client and are listed in the README, including the Python -> Rust -> Python -> Rust back-and-forth that proves the rollback path rather than asserting it. Python backend untouched and still the live runtime; contract suite 446/446 after this work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
cf961603fe |
openfut-adapter-fifa17: FIFA 17 Blaze adapter, oracle-tested
The second migration step: the layer above the codec, deciding WHAT to say
rather than how to encode it. Sits on openfut-protocol-blaze and supplies
what that crate deliberately refuses to know.
blaze/ids.rs component/command/notification tables
blaze/config.rs injectable identity + endpoints, nothing hardcoded
blaze/session.rs per-connection state
blaze/client_config.rs the fetchClientConfig tables
blaze/responses.rs 16 Blaze::* response bodies
blaze/dispatch.rs (component, command) -> Vec<Frame>
Parity is tested, not asserted. fixtures/generate.py drives the real
blaze_responder_v3b.dispatch() and records 49 request->response(s)
transactions, replayed in order against a shared session per connection so
ordering-dependent behaviour is exercised: preAuth captures the locale later
ALOC fields echo, login sets the auth code getAuthToken returns. Comparison
is byte-for-byte including frame count and order.
98 tests green across both crates; clippy clean.
MUTATION TESTED, and it found a real defect in this commit's own design.
Swapping two post-login notifications and flipping one enum inside
AccountInfo both turned the suite red as intended. Hardcoding an address in
utas_base() did NOT -- the config templating substituted raw hosts directly,
making those helpers dead code that merely looked load-bearing. The table now
templates on URL-level tokens ({utas_base}, {nucleus_base},
{pow_content_url}) so they are the single place a URL shape is defined, and
the mutation is caught.
The client config table (227-243 rows per CFID) is generated from the oracle
rather than transcribed: it is reverse-engineered data, not logic, and 400
hand-copied string literals would add a typo class no reviewer can catch. The
generator substitutes real addresses back in and diffs against the oracle for
every section before writing, so the templating is verified rather than
assumed.
Reproduces one known defect deliberately: nucleusConnect is built from BIND,
not advertise, so the live split deployment tells a client on another machine
to reach Nucleus at http://0.0.0.0:42131. Confirmed against the running
container. Reproduced because it is what the only proven-working config does;
fixing it needs live validation and is a separate change. It also implies the
Nucleus stub is not reached in the current remote flow.
Blaze carries no FUT domain state -- no coins, packs, clubs or squads on this
wire -- so Session stays a session key, locale, service name, auth code and a
flag. That boundary will need defending when UTAS is migrated.
Not wired into anything. The crate answers frames; it opens no socket and
owns no runtime. The Python backend remains the live service and the oracle,
and is unmodified (contract suite still green).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
cc3ecddc06 |
openfut-protocol-blaze: pin advertise/bind so fixtures do not depend on the shell
The responder reads OPENFUT_ADVERTISE/OPENFUT_BIND at import time and several live payloads embed the advertised address (Blaze redirect target, RS4/POW/ roster URLs). Without pinning, regenerating on a machine that exports OPENFUT_ADVERTISE produces different bytes and --check goes red for a reason that has nothing to do with the codec. Pinned to 127.0.0.1 as a placeholder so no real LAN address is baked into a committed fixture. Not a claim about deployment: remote mode still requires an explicit advertised address and has no loopback fallback. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a9a816e0ed |
openfut-protocol-blaze: generic Blaze protocol layer, oracle-tested
First Rust component of the Python -> Rust migration. Chosen first because
it is the lowest genuinely game-independent layer, it has an executable
oracle, and both existing Rust implementations of it are wrong.
Contents:
* fire2 -- the proven 16-byte frame header, frame/stream splitting
* heat2 -- tag packing, varints, all 11 TDF value types
* message -- frame + decoded body, routed by NUMERIC component/command
* diagnostics -- dumps for capture review
No FIFA 17 command tables, response schemas or notification IDs: this layer
knows 0x0009/0x0007 is component 9, command 7, not that it means
Util::preAuth. That mapping belongs to a game adapter, which is what lets a
future FIFA 18/23 adapter reuse this.
Parity is tested, not asserted. fixtures/generate.py drives the proven
Python responders (heat2.py, blaze_responder_v3b.py) and freezes 56 vectors
-- 31 of them real payloads from the responder's own builders, including
the 11.8 KB preAuth reply. tests/oracle_parity.rs replays every one
byte-for-byte. 54 tests green; clippy clean.
Supersedes two wrong framings, neither of which is removed yet:
* fifa-blaze/crates/blaze-proto/frame.rs -- a 12-byte header with a u16
length, nibble-packed type/options, an error field and a JUMBO flag.
A documented guess at FIFA 23 predating the FIFA 17 recon.
* heat2.py::build_fire2_frame -- packs >IHHHHB3s, msgId at [10:12] and
msgType at [12]. Dead code, but its docstring still states that layout.
Confidence is carried in the types: TypeId::is_verified() reports which
layouts are capture-backed (int/string/blob/struct) and which the oracle
marks UNVERIFIED (list/map/union/varlist/objtype/objid/float), with a test
asserting the unverified ones stay flagged.
Cargo.lock is deliberately NOT included: it re-resolves ~240 lines against
the current registry even without this crate, so that churn is pre-existing
and does not belong in a foundation commit.
The Python backend remains the live runtime and is untouched. Nothing
consumes this crate yet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3153a93edf |
fifa17-recon: drop superseded docker-side tools/data copies
fifa17-recon/tools (authoritative) and fifa17-recon/data now feed the Docker build directly via the curated runtime-tools.list manifest. The duplicated fifa17-python/tools+data are removed so the repo has a single source of truth; the rebuilt openfut-fut-backend:dev image is byte-identical to the previous deployment (verified: manifest diff empty, 446/446 contract checks pass). |
||
|
|
f64106ed8b | fifa17-recon: fix compose dockerfile path for relocated build context | ||
|
|
9faaf12dd7 |
fifa17-recon: Docker build consumes authoritative tools via curated manifest
Build context moves from docker/fifa17-python/ up to fifa17-recon/ so the Dockerfile reads the single-source tools/ and data/ trees. Only the 77 runtime files listed in runtime-tools.list are installed into /app/tools (baseline image minus the two git-ignored certs, regenerated in-image). memdump and recon artifacts are excluded via fifa17-recon/.dockerignore. |
||
|
|
83539e33ec |
fifa17-recon: take running-backend versions of 8 runtime files (direction fix)
The earlier reconcile committed the local working-tree versions of these files, which are OLDER than the deployed backend. The running container (C) is byte-identical to docker/fifa17-python/tools (B) and is a strict superset: it adds profile_path_for/select_account/ensure_security_question (fut_store), safe_header_for_log/safe_request_path/security_question_route (utas_server), account_sync_route/_match_call/match_ready_body, plus POW balance fields and match lifecycle support, with zero unique local functions lost. Reconciled tree is now a strict superset of B with every shared file byte-identical; verified via md5 map (0 missing, 0 differing). |
||
|
|
695421cfd4 | Merge remote-tracking branch 'origin/main' into fifa17-fut-squad-and-userinfo | ||
|
|
8cba70dc90 |
fifa17-recon: reconcile authoritative tools with running backend (B)
- Add 8 files present in docker/fifa17-python/tools but missing from the top-level tree: fut_accounts.py + 7 test_*.py contracts (all committed in the server's docker tree; byte-identical to the running image). - Preserve newer responder work already matching the running container: utas_server.py (offlineSeason), lsx_responder_v2.py (OPENFUT_BIND), blaze_responder_v3b.py, autopatch.py, pow_server.py, fut_store.py, test_fut_contract.py, fifa17-hook-m1.sh. - Add 30 newer ghidra_queries (draft purchase/state, SBC 9-26, runtime registries). Local tree is now a strict superset of B with all shared files byte-identical. |
||
|
|
28773e7cf1 |
fifa17-python: sync tools to running container state
The frozen baseline image predates two hot-patches made in the running container after build: * utas_server.py: FUT_MODES-gated offlineSeason block in GetHubData's club response (keeps the offline-season summary valid) * test_hub_offline_season_contract.py added to /app/tools Sync fifa17-python/tools to the running container (verified byte-identical, 237 files incl. the redir cert pair) and snapshot the live FS as openfut-fut-backend:python-running-2026-08-10 (docker commit). A fresh build from the committed sources now reproduces the running backend exactly (baked SHA256SUMS.txt diffed against the container manifest: identical).python-fifa17-baseline-2026-08-10 |
||
|
|
3ae5587a38 | docs: baseline manifest equivalence note (pycache + cert deltas expected) | ||
|
|
70a64e3709 |
fifa17-python: commit working FUT backend deployment (client/server split)
Freeze the running offline FUT backend into version control as fifa17-recon/docker/fifa17-python/ - declarative and rebuildable from a fresh checkout: * OPENFUT_BIND / OPENFUT_ADVERTISE client/server split in the responders (lsx, blaze, roster, utas, pow) + entrypoint.sh; OPENFUT_ADVERTISE is required for remote mode (compose and entrypoint fail without it) * docker-compose.yml reproducing the frozen baseline container exactly (env, ports incl. the 8085->8080 POW-content remap, /state bind, restart) * .env.example / .env for site config - the LAN IP is never hardcoded in source * tools/ + data/ staged from openfut-fut-backend:python-baseline-2026-08-10, verified byte-identical to the running container at freeze time * client_arm.sh (the 105 client-side arming counterpart) * Dockerfile bakes /app/SHA256SUMS.txt so any image is self-identifying * docs/BASELINE-python-2026-08-10.md: frozen image/container/hash record, restore instructions and rebuild-equivalence procedure Secrets (redir key/cert, .env) and runtime state (docker/state) stay gitignored. The live container is untouched pending the .105 launcher audit. |
||
|
|
622a774f6a |
chore: update openfut-launcher submodule to feat/sbc-hook-tracing branch
Tracks SBC hook tracing PR #1 for FIFA 17 reverse-engineering |
||
|
|
cc694774a3 | wip: checkpoint FIFA 17 SBC research for Windows migration | ||
|
|
3d3239bab9 | feat: document and stage FIFA 17 SBC hook workflow | ||
|
|
a7e3e43ae9 |
fifa17-recon: the refusing modes have no server fix, and the hub-atom lead is cosmetic too
Completed the refusing-modes workflow (ground truth + 4 per-mode investigations + adversarial verify each + synthesis). All four mode families -- Seasons, Draft, SBC/Objectives, Tournaments -- are NOT_SERVER_REACHABLE, HIGH confidence, all four adversarial refutations failed. Live re-confirmed on pid 24653 (slide proven via FNV control): every named mode-gating byte reads ENABLED=1 (IS_FRIENDLY_SEASON_ENABLED +0x1fd3a, IS_TOURNAMENT_QUIT_ENABLED +0x1fd3b, IS_DRAFT_MODE_ENABLED +0x1fd3d, plus the unnamed offline-draft-enable +0x1fd3e) yet the tiles stay greyed. The new lead this pass added -- do the six /hub mode sub-objects gate availability? -- is refuted: friendlySeason/offlineSeason/onlineSeason/draftSummary/tournament/ tournamentProgress carry only stats and display strings, no enabled/available/ unlocked atom. They are cosmetic, exactly like hub.tradePile. The one server-writable input that exists (friendlySeasonsEnabled -> +0x1fd3a via applier FUN_18011dc50) has its sole reader in the packed FIFA17.exe front-end via a vtable getter with no CardsDLL caller, and it is already 1. The refusal is decided in the Denuvo-packed Frostbite front-end, which has no server surface. docs/plan-2026-08-06-refusing-modes.md: full evidence chains, gate-byte table, the six sub-deser field maps, per-mode verdicts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lrx9to3pihN6Sm9sXgc8np |
||
|
|
31fc590b99 |
fifa17-recon: the FUT-hub Transfer List tile counts, and the hub parser is NOT reflection
The Transfer List hub tile read "0 items / Selling 0" while a card was actively
listed. Enumerating the /hub parser FUN_180139610 straight from the on-disk
CardsDLL (objdump) refutes the old ENDPOINT_MAP claim that it uses C++ reflection
with "no atom ladder, nothing to enumerate": it has an ordinary running-sum atom
ladder reading 18 atoms. The tile is fed by hub.tradePile (0x333), a nested object
(sub-deser 0x18013ead0) reading count/selling/sold as scalar ints -- the same
scheme as GetAuctionCount, so serving it in the hub body is freeze-safe. The tile
never re-polls the standalone /tradePile/counts, which is why fixing that endpoint
alone did not move the tile.
Also: the hub tile polls LOWERCASE tradepile/counts while the Transfer List screen
uses camelCase tradePile; our case-sensitive routes matched only the screen, so the
tile's counts call fell through to /trade and got a shape the counts deser skips.
Made the tradePile routes case-insensitive.
And bake the proven transfer-market flags (FUT_TRADING/PILESIZES/TRADEABLE/
DISCARD_TABLE/DISCARD_SEND) into openfut-fut.sh so a plain `start` brings up the
working state instead of regressing trading to greyed-out.
- tools/utas_server.py: hub_data() serves tradePile:{count,selling,sold};
tradePile routes now re.I
- tools/openfut-fut.sh: utas launched with the working flag set
- docs/ENDPOINT_MAP.md: full 18-atom hub map + tile map, correction of the
reflection claim
- tools/ghidra_queries/objdump_atom_ladder.py: the objdump-based atom-ladder
decoder used to derive the above
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lrx9to3pihN6Sm9sXgc8np
|
||
|
|
245c22161b |
fifa17-recon: correct tradePile/counts shape, and narrow the marketdata array fix
Two follow-ups on the working transfer market.
1. GET /tradePile/counts now returns the FutGetAuctionCount shape
({count, maxAuctionsAllowed, offered, selling, sold}, all scalar ints, atoms
0xbc/0x1bf/0x1e5/0x2b8/0x2c9) via a dedicated route ordered before /tradePile.
Previously it fell through to tradepile_route and got the auction-LIST body, which
the counts deser skips, leaving every tally at its constructor default. Survivable
but wrong; the doc flags the loaded byte at +0x28 as gating a completion-handler
branch. selling reflects real STORE.listings().
2. Narrowed the marketdata bare-array fix to /pricelimits only. The client sends TWO
marketdata requests: /marketdata/pricelimits (GetSuggestedPricing, a bare array,
the thing that froze) and plain /marketdata?defId=N (price comparison, an OBJECT).
The prior commit returned the array for both, which the contract suite caught
(test_market_bodies: 'list' has no attribute get) -- plain /marketdata wants
{minPrice,maxPrice} and was never the freeze. Returning the array for it would be
the same desync in reverse. Now: pricelimits -> array, plain marketdata -> object.
The contract suite catching my over-broadened fix before it reached the game is the
suite doing its job. 439 contract checks pass, market unit suite passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
43557989f5 |
fifa17-recon: the transfer market works -- listed a card end to end, no freeze
The subsystem that was fully greyed-out this morning now lists a card on the transfer
market: price screen, Submit, "your item is now up for trade", TRANSFER LIST 0/100,
auctionCount 1, and STORE.listings() holds the auction. Every step verified at the
instruction level first, then confirmed live. Three fixes, all behind flags, all off by
default until this run proved them.
1. WE WERE BANNING OUR OWN TRADING. userInfo.feature (atom 0x11c) is a RESTRICTION map,
not a grant; we sent feature={"trade":true}, which is a trade BAN. Verified in
q_feature_trade.py: FUN_18013ec10 parses feature/trade into userInfo+0x17c, and at
the massinfo END_OBJECT the client runs
cmp byte [rsi+0x17c],0 / jz skip / mov dword [rsi+0x50],0
feeding applier 0x18011dc91 -> IS_TRADING_ENABLED (model+0x1fd2e) = 0. It runs LAST
and unconditionally, which is why the gate read 0 all day regardless of /settings or
the Blaze config store. FUT_TRADING sends feature={} instead. Live: gate flipped
0 -> 1 on UT re-entry (model rebuilt, pointer changed, byte read 1).
2. TRANSFER LIST CAPACITY 0/0. pileSizeClientData (massinfo atom 0x227, parser
0x18013adb0) is the capacity, NOT the "MY CLUB counter" the old comment claimed.
Verified in q_pilesize_keys.py: exactly two storing arms, key 2 -> model+0x1fd1c
(TRADE_PILE_SIZE) and key 4 -> +0x1fd20 (watch list), every other key SKIP'd. The old
code would have sprayed the 246 club count into the capacity. FUT_PILESIZES sends
key 2 = 100, key 4 = 50. Live: capacity read 0 -> 100, header showed 0/100.
3. THE PRICE SCREEN FROZE THE CLIENT. GET marketdata/pricelimits was answered with an
OBJECT {minPrice,maxPrice}; the deser 0x180163ee0 reads a BARE TOP-LEVEL ARRAY
(root loop while tok != 0xd), so object-where-array desynced the SAX reader into the
0x1801c7f1a busy loop (confirmed live: utime climbing 227 ticks/s, core pinned).
Verified in q_pricelimits.py: element fields defId 0xcf, maxPrice 0x1c2, minPrice
0x1ca, all scalar ints. marketdata_route now returns a bare array, one element per
requested defId. Live: price screen opened and Submit succeeded.
Corrected along the way, all now in the code: two prior "trading root causes" from
earlier today were wrong (the Blaze IS_TRADING_ENABLED keys are output-only names, and
the applier is a virtual method at vtable+0x988, not unreachable). Those refutations are
recorded in blaze_responder_v3b.py and the doc.
Also lands the transfer-market recon doc (plan-2026-08-06-transfer-market.md) and the
market Ghidra query set.
Server-authoritative economy note: the 5% transfer fee and the price bands (currently a
150..15000 placeholder per defId) are not yet real; that is refinement, not a freeze.
The live-auction market SCREEN ("List on Transfer Market" browse) is a separate surface
still to do (P4 auction-counts route, P5 empty market bodies).
Live: 439 contract checks pass. Card listed and persisted, auctionCount 1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
a3fd51692f |
fifa17-recon: the trading gate is still shut, and two of yesterday's conclusions were wrong
Ships the club-item subtype correction and the tradeable plumbing, and records two
refutations of claims made earlier in the same session. Nothing here is a working fix
for trading; the honest state is that the gate is still closed and we now know more
about why.
REFUTED 1: "the Blaze client-config store opens the trading gate". It does not, and the
flag is inert. IS_TRADING_ENABLED is an OUTPUT NAME. FUN_18006cc60 is a publisher: at
0x18006ccc6 it calls [rax+0x270] to READ gate byte 0x1fd2e, then lea rdx,[
IS_TRADING_ENABLED] and hands the value out under that name. The only rip-relative
reference to the literal 0x1801fc118 in all of .text is that lea; there is no comparison
against it anywhere, so no client-config key of that name can be read as an input. That
also undermines the IS_* store keys shipped beside it: their apparent success was never
actually attributed to them.
REFUTED 2: "the gate byte flipped to 1". It reads 0. It was measured as 1 shortly after
CardsDLL mapped and that was over-claimed as a success; a thorough re-measurement read 0
on the SAME pid and model pointer, and a fresh session reads 0 with an unambiguous raw
dump (model+0x1fd18.. = 01000000 00000000 00000000 00000000 3c000000 01 01 00 01, the 00
being 0x1fd2e). Either the first read was transient or something clears it after login.
The only writer is FUN_18011dc50 at 0x18011dc91, so a 0 means something RAN and wrote it.
AND THE "/settings IS DEAD" CLAIM FALLS TOO. FUN_18011dc50 is not unreachable: it is a
VIRTUAL method at model vtable slot +0x988 (absolute pointer 0x18021cc28). A direct-call
search found no callers because Ghidra does not resolve virtual calls, which is the same
dispatch-form trap that has now produced seven wrong verdicts here. The real chain is
settings response -> FUN_180174630 -> FUN_18013c6d0 (deser)
-> completion callback FUN_180173e00 -> vt+0x988 and vt+0x998 -> gate bytes
and FUN_180173e00 bails before applying anything unless the int at response+0x1c is
zero. Which atom writes +0x1c is unknown and is the thing worth chasing.
The measurement behind that claim also had a gap: it checked +0x1fd14, +0x1fd4c and
+0x1fd54 for the maximumTradePileSize=77 probe but NOT +0x1fd1c, which is the actual
TRADE_PILE_SIZE (read via vt+0xa58 = FUN_18011bf30). So the probe never tested the field
it needed to. Serving 77 and reading +0x1fd1c is the clean falsifier and is still open.
Recovered and worth keeping: an authoritative slot-to-name table from the publisher.
vt+0x270 IS_TRADING_ENABLED -> +0x1fd2e vt+0x2b0 IS_FRIENDLY_SEASON_ENABLED -> +0x1fd3a
vt+0x2b8 IS_TOURNAMENT_QUIT_ENABLED -> +0x1fd3b vt+0x2c0 IS_PROCESSING_STATE_ENABLED -> +0x1fd3c
vt+0x2c8 IS_DRAFT_MODE_ENABLED -> +0x1fd3d vt+0x2d8 IS_STORY_MODE_REWARD_ENABLED -> +0x1fd3f
vt+0x2f0 IS_RETURNING_USER_REWARDS_SCREEN -> +0x1fd40 vt+0xa58 TRADE_PILE_SIZE -> +0x1fd1c
That also locates the red TRANSFER LIST 0/0: it is +0x1fd1c, currently 0.
WHAT IS ACTUALLY SHIPPED HERE, all default off:
* FUT_TRADEABLE sends untradeable=false. Verified landing at item+0x49 (stored
INVERTED by case 0x361) on a live club record. Applied on every READ path, not only
in _item(), because the save holds 246 items minted before the flag existed and the
club route serves them straight from the save. That gap was caught by reading the
served JSON, not by unit-testing the factory.
* FUT_TRADING adds tradingEnabled and IS_TRADING_ENABLED to the Blaze config. Kept
only as a record of the refutation, with the reasoning inline so nobody retries it.
* fut_clubitems FAMILIES subtypes corrected: kit 9, stadium 10, badge 11 (cardtype 7,
not 9), ball 30, league logo 31. Every previous value sat in the 0x91..0x96 TROPHY
block. probe_shelf's candidate set lacked 9, 10 and 11, so the probe route the docs
preferred could never have answered this for three of five families.
* Club kits and badges now carry teamid, reintroduced ALONE after the 2026-08-05 crash
(which was never bisected; value is the established suspect and that response also
carried 30 items across five wrong subtypes). itemType dropped: it was unobserved and
never copied into the record.
Live: 439 contract checks, 414 card-family checks, market suite, all pass. The transfer
market still refuses with zero requests and the menu entries are still greyed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
e578443d73 |
fifa17-recon: tradingEnabled is 0, and that is why the transfer options are greyed out
Card-subsystem pass, 11 agents plus three adversarial verifiers. Full writeup in docs/plan-2026-08-06-card-subsystem.md. Two of the results below correct things I committed earlier today. THE GREYED-OUT TRANSFER OPTIONS ARE EXPLAINED. "Place on Transfer List" and "List on Transfer Market" have been disabled in the reveal screen and nobody knew why. TO_TRADE_PILE (FUN_1801a7260) requires BOTH item+0x49 tradeable AND a service gate at vtable slot +0x270. That slot is `movzx eax, byte [rcx+0x1fd2e]; ret`, and 0x1fd2e is the tradingEnabled gate byte. Read live and reproduced independently: slot +0x2b0 friendlySeasons disp 0x1fd3a VALUE=1 slot +0x2c8 draftMode disp 0x1fd3d VALUE=1 slot +0x2e0 packOpeningAnim disp 0x1fd45 VALUE=1 slot +0x270 tradingEnabled disp 0x1fd2e VALUE=0 tradingEnabled is the FIRST gate byte found that is not 1. This partly rehabilitates the settings work from this morning: that plan died because every gate it targeted already read 1, and the conclusion drawn was that the settings array does not matter. It does. It matters for a flag nobody was looking at, and tradingEnabled is ALREADY in _SETTINGS_KEEP, plumbed and never sent because _SETTINGS_MODE defaults to off. So the fix is two things, not one: FUT_SETTINGS=keep AND untradeable false. Shipping only the boolean would look like the finding failed. THE DISCARD "MISS" NEVER EXISTED, which corrects |
||
|
|
e3092ca0f9 |
fifa17-recon: the client now shows the quick-sell value it is actually paid
Follow-on to
|
||
|
|
21a81ad63c |
fifa17-recon: the real quick-sell table, and the grouping bug is not in our layer
Multi-agent pass over the store subsystem, 11 agents, findings run through three
adversarial verifiers. Full writeup in docs/plan-2026-08-05-store-subsystem.md.
THE REAL DISCARD TABLE IS RECOVERED. quick_sell() paid an invented rating tier
(600/300/150/50) that was wrong for every single card. The real table is
fcc_discardcoins in the client's own game DB, 141 rows keyed (cardtype, level, rare),
read out of the running client and verified 22/22 against live items:
value = round_half_up(rating * price / 100)
level = 3 if rating >= 75, 2 if 65..74, else 1 (0x180141e8a..0x180141ea3,
derived from rating, NOT a wire field)
cardtype = FUN_1800d8330(cardsubtypeid), decoded from its jump table and checked
across every subtype 0..599 with zero disagreements
A 94-rated gold rare is 752, not 600. A 76 rare is 608, not 150. A 55 bronze is 17,
not 50.
This also closes a disagreement nobody had noticed: the CLIENT already computes and
displays the correct value locally whenever our discardValue (atom 0xd7) is 0 or
absent. FUN_18013fe00 stores our value at item +0x38 and the guard at 0x180141025
skips the local computation when it is non-zero. So the screen has been showing the
real number while the server paid a made-up one, on every quick sell ever made.
Verified beyond what the report claimed, because a missing table row pays ZERO and
that would be a regression the old flat tier could not produce: across all 236 items
in the live profile, 230 map to cardtype 1 and 6 to cardtype 6, and NOT ONE would pay
0 coins. Table reproduces at 141 rows and the worked example lands exactly.
ZERO WIRE CHANGE, FUT_DISCARD_TABLE default off. Nothing new is sent; only the coin
figure the server credits moves. This is the patch worth defaulting on after one
in-game check, which is simply quick-selling a card and seeing the coins paid match
the value the card was already displaying.
THE GROUPING BUG IS NOT IN CARDSDLL, and the fix ranked first would have wasted a
launch. Live in the running client all three display groups own exactly the right
pack, there is exactly one copy of each pack record in 4 GiB, and nothing we send is
mis-parsed. The parsed model is correct and the Scaleform layer picks the wrong pack
when turning a tile click into a category id. displayGroupAssetId is served as 1/5/6
while the screen's category field reads 3, and group tiles carry a hardcoded
CATEGORY_ID of 0. Confirmed by direct read: ordinal 3, assetId 6, i.e. Premium, while
the last click was Gold.
The heap map that made this possible, all scoped to one pid: display-group vector
control block, 3 elements of 0x108; group record fields at +0x00 sortPriority,
+0x04 displayGroupAssetId, +0x40 a one-element pack vector; inner pack record 0x1a8
with packType at +0x38, ids at +0x70/+0xac, price at +0xa0, quantities at +0xc0..+0xd0.
extPrice SHOULD BE DELETED, not corrected. Both sub-parsers read only
externalPriceId; amount and currency are discarded. Sending the key at all creates an
"mtx" currency row that switches on a real-money price line the client can never fill
offline, which is the literal "or %1s" on every tile.
A WORRY NOBODY HAD RAISED, and I confirmed it from our own logs: the client has sent
packId 6 on every purchase it has ever made, four for four tonight and six for six
across history. We have never observed a successful buy of anything but Premium Gold.
Also settled: FUT_STORE_DISPLAYGROUP=0 is the right resting state, argued from
mechanism rather than from history; FUT_USERINFO=packs stays off because the
unopened-pack counter is client-mutable and the flag ladder silently drops squadList;
POST /user is a latent hard freeze that has never fired because the client never
issues that POST.
Honest coverage: the ActionScript layer is unread by everyone and every remaining
store mystery lives there.
Live: 439 contract checks pass, market suite passes, both flags off.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
3f3d5704a7 |
fifa17-recon: quick sell has never been reachable, and finalFunds is the rendered price
Live run, 2026-08-05 evening. Three results, one of them a route that has been dead for
the whole life of the project.
QUICK SELL WAS NEVER SERVED. Captured on the wire:
DELETE /ut/game/fifa17/item/100000240 (single card, id in the URL, no body)
ENDPOINT_MAP documented the path as `ut/delete/game/%s/item`, ROUTES was built from the
doc, and the regex therefore never matched a real quick sell. Every quick sell fell
through to the catch-all, so quick_sell_route() and STORE.quick_sell() behind it had
never once been called.
The empty response was not a harmless no-op. This body is BALANCE-BEARING: the client
takes its coin total from it, so with totalCredits absent it rendered an uninitialised
value. A real session showed 1,133,686,384 coins against a true balance of 9,889,600.
Display artifact only, corrected by the next GET /user/credits, and the save was never
touched, but it is the reason a stub is not acceptable here.
quick_sell_url_route() serves the real form and returns the corrected shape,
{"items":[{"id":N}],"totalCredits":N}. DEFAULT ON, which the house rule now permits:
two quick sells fired through it in one session, each credited 150 and removed the
card, and the coin arithmetic reconciled exactly against the 15,000 pack purchases
either side of them. An unknown id returns {"items": []} rather than claiming a sale we
cannot account for.
Still UNKNOWN and deliberately not chased: whether the client ASSIGNS totalCredits as
the new balance or ADDS it as a delta. We send the new balance. The discriminating
window is about five seconds wide, because the client refetches GET /user/credits
straight afterwards, so either reading self-corrects and the practical impact is a brief
wrong number. It mattered only while we answered {}, because that garbage persisted.
The credit amount is STORE.quick_sell()'s invented rating tier, not FUT's real discard
table, which remains unknown.
finalFunds IS THE RENDERED COIN PRICE. Served funds=15000 / finalFunds=4321 on one pack
and the tile read 4,321. funds is not displayed. ENDPOINT_MAP updated to CONFIRMED LIVE
with the method recorded. FUT_PRICE_PROBE, the flag that produced it, stays default off
and is disarmed: it puts a price on a tile that the buy path does not charge.
TWO UNPLANNED FINDINGS, both recorded for the next round rather than fixed here:
* The store grouping is broken and it is NOT cosmetic. All three packs collapse into
one display group, and the Bronze, Gold and Premium group tiles all drill into the
same single Premium Gold pack, so TWO OF THREE PACKS CANNOT BE BOUGHT. We send
displayGroup {"value": name} but never displayGroupAssetId (0xda), so everything
lands in group 0. The docs had this parked as a cosmetic "tiles read unknown"
issue; it is an availability bug.
* The FIFA Points price renders as the literal "or %1s", an unsubstituted printf
placeholder. extPrice.finalPrice is served as {"amount":N,"currency":"mtx"} and
"mtx" is evidently not a currency token the client resolves. Cosmetic.
Corrected in passing: packContentInfo DOES reach the tile (11 ITEMS / 11 GOLD /
11 RARES against exactly what we serve). An earlier screen showing zeros was the
display-GROUP level, which carries no content info. D3 was right.
Live: 439 contract checks pass, both probe flags off, store prices back to honest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
89da7b7609 |
fifa17-recon: /hub refutes yesterday's envelope conclusion, and two ENDPOINT_MAP freezes
Three things: the envelope rule was wrong and is corrected, /hub is settled, and two
documented response shapes that would freeze the client are fixed.
THE CORRECTION. The previous commit concluded that a three-token root consumes `{`, the
first field name and that field's value without dispatching them, so the first key of a
flat body was silently eaten, and that `login` had therefore never been delivered on
POST /user. That is WRONG and is withdrawn, along with the claim that the key order of
the auth dict is load-bearing.
The first call to FUN_1801c7f10 returns token 7 and consumes NO input. It is a
once-only start-of-document token, guarded by the flag at parser+0xda together with the
zero character counter at parser+0x30. So the three tokens are BOF, `{`, and the FIRST
FIELD NAME, and the key loop dispatches from that first key onward. The `== 10` test on
the third token is not an envelope check, it is the empty-object early-out: for `{}` the
third token is END_OBJECT and the root exits with its constructor defaults intact, which
is why answering `{}` has always been safe.
Corrected enum: 7=BOF 9=START_OBJECT 10=END_OBJECT 11=FIELD_NAME 12=START_ARRAY
13=END_ARRAY. The enum itself was right before; the inference from it was not.
HOW IT WAS CAUGHT, which is the part worth keeping. Not by more decompiling. /hub is
served flat and the wrong model predicted its first key would be discarded, so the
prediction was checked against the client's own memory: clubPlayers read back as 205,
the value the server sent, at model+0x1fd70+0x3c with the slide proven against the FNV
prologue first. One live read refuted a chain of otherwise sound static reasoning in
about a minute. tools/hub_counter_probe.py keeps it repeatable.
Consequence worth flagging: a wrapper is not just unnecessary for these roots, it would
be harmful, since a wrapper key hashes to an atom with no arm and the whole object is
skipped. That makes the createPackResponse envelope DOUBTFUL rather than confirmed.
Atom 0xbe has no arm in FUN_180162880. There is no live evidence either way because
nothing has ever parsed that body, so the buy path is left exactly as it is.
TWO ERRORS OF MINE ON THE WAY, both recorded in the doc because both are cheap to
repeat. I searched for RS4:FutGetHubServerResponse, found nothing and reported that no
hub class existed; the class is FutGetHubDataServerResponse (literal 0x18022ce40,
vtable 0x18022cd48, deser 0x1801738b0, control FutSquadSave -> 0x180171a60 matched in
the same run). Then I scanned 152 deserializers for clubPlayers, got zero hits and a
passing control, because the guard is `!= 0x90` and my pattern only matched `== 0x`.
The control passed only because auctionCount happens to use `==`. A control that does
not exercise the same code shape as the target is not a control. The comment already at
utas_server.py:1076 had the hub chain right the whole time.
ENDPOINT_MAP corrections, both freeze-risky as written, neither affecting what we serve
today:
* duplicateItemIdList is an ARRAY OF OBJECTS (element deser 0x180138e10), not the int
list at :1095. Bare ints where the element parser expects objects is a tokenizer
desync, i.e. a hard freeze at 0x1801c7f1a. Control that this is not a misread:
dreamSquads 0xe9 in FutMoveCard genuinely is a bare int array.
* FutDiscardCardServerResponse is {"items":[{"id":N}],"totalCredits":N}. There is no
top-level id.
No behaviour change. utas_server.py is comment-only. 439 contract checks pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1605e6effd |
fifa17-recon: the envelope rule, and the one key of the auth body that is silently eaten
The open question was whether a response deserializer DESCENDS a wrapper or PROBES for
one. Three roots spend an identical three tokenizer calls before dispatch, yet we serve
some bodies wrapped and some flat, and not all of those could be right.
TOKEN ENUM, decoded from the class table at DAT_18023dd40 and the switch in the
classifier FUN_1801c67a0 (the push/pop arms key off container state 2 = object,
3 = array):
9 START_OBJECT case 0x64, pushes state 2
10 END_OBJECT case 0x65, pops state 2
11 FIELD_NAME confirmed independently: FUN_18013bd40 tests +0xd0 == 0xb then
atom-hashes the string at +0xf8
12 START_ARRAY case 0x66, pushes state 3
13 END_ARRAY case 0x67, pops state 3
1 error the caseD_78 sink
So the three tokens are `{`, the first FIELD_NAME, and the token opening that field's
value. The envelope is structurally required and its name is NEVER hashed, which is why
FutCreatePack's ladder has no arm for createPackResponse (0xbe) and does not need one.
Coverage for that absence: the ladder has exactly four arms (0xec, 0x16e, 0x1dd, 0x264)
and 0xbe does not occur anywhere in the full 4702-char decompile, printed in full.
The competing reading rested on a factual error. It claimed the /purchased root spends
the same three tokens. FUN_180124ee0 spends TWO and hands off to FUN_18013bd40, which
spends the third. Same total, split across two functions. /purchased never was a
counterexample.
THE BUG THIS FOUND IS NOT THE ONE THAT WAS PREDICTED. The doc expected starterPack,
squad and userData to be swallowed on POST /user. They are not. FutCreateUser
(0x18014cc60) has ladder arms for exactly the five keys we send, and four of them
dispatch correctly at the outer level. The one that does not is `login`: its name is
eaten as the anonymous envelope and its value as the third token. It has an arm, so the
client wants it, and it has never once been delivered.
The second-order consequence matters more than the first. The key order of that dict is
load-bearing and nothing said so. Put userData first and the client loses the entire
user record, silently, with no error and no log line. That warning now sits in the code
next to the dict, which is the only place someone about to reorder it would look.
No behaviour change here. The utas_server.py edit is a comment. 439 contract checks
still pass. The probable proper fix, wrapping all five keys one level down inside a
single envelope key, is a hypothesis with a mechanism rather than a proven fix, and it
touches the login path, so it is not made here and would go behind a flag defaulting
off.
Writeup is section 2 of docs/plan-2026-08-05-pack-opening.md, added by the previous
commit. Opened by this and still UNKNOWN: GET /hub is answered with a flat two-key
body, which under this rule a three-token root would silently truncate, but there is no
FutGetHubServerResponse class and neither atom has a code xref, so /hub may not go
through a generated root at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
afdbb364ca |
fifa17-recon: pack opening reversed end to end, and there is no pack-inventory endpoint
A twelve-agent pass over the parts of pack opening we did not understand, run against
the live client (CardsDLL slide proven, not assumed) plus static CardsDLL. Findings
below survived an adversarial verification round that corrected several of them; where
a verifier and a finder disagreed, the verifier won.
THE HEADLINE IS A NEGATIVE, and it deletes work rather than creating it. There is no
pack-inventory endpoint in FIFA 17 and there never was. Proven three independent ways:
the 48-entry UTAS route template array at 0x18021df80, a regex for "ut/" over the whole
PE, and the 125-row client action table at 0x1802caa20, which is the complete set of
requests the client can originate. "Serve the pack inventory" comes off the backlog.
The unclaimed-pack tile and My Packs are two fields on responses we already build.
Corrections to ENDPOINT_MAP.md, both freeze-risky as written:
* duplicateItemIdList is an array of OBJECTS (element parser 0x180138e10: itemId
0x16d, duplicateItemId 0xeb, itemLoans 0x16f, duplicateItemLoans 0xed), not the
int list documented at :1095 and :218. Control that this is not a misread:
dreamSquads 0xe9 in FutMoveCard genuinely is a bare int array and parses with no
inner object loop. We serve [], so this is a docs bug today and a live freeze the
moment somebody implements it from the map as written.
* FutDiscardCardServerResponse is {"items":[{"id":N}],"totalCredits":N}. There is no
top-level id. :968-971 is wrong twice over.
packContentInfo is DECORATIVE. It is read only into a store-tile view model, and
nothing compares the declared counts against the delivered itemList, so open_pack()
does not have to honour the distribution.
The reveal is entirely CLIENT-SIDE. Walkout, tiering, colours and ordering are
arithmetic over fields we already send. Genuine outstanding server work reduces to
three items: duplicates, quick-sell credit, unopenedPacks.
Perishable intel captured: the real FIFA 17 retail pack catalogue, 41 SKUs with Origin
offer ids, recovered from the client heap as a parsed copy of data/store/storecfg.xml.
It is in no file on disk, only in a running process.
futmem/ is a standalone read-only Rust crate for this kind of work (maps, find,
strings, read). Read-only by construction: it opens /proc/<pid>/mem with File::open
and there is no code path in it that can write to another process, because a live game
session depends on that. Its own [workspace] table keeps it out of the parent
workspace. Chunked scanning overlaps by pattern_len-1 so a match spanning a chunk
boundary is still found.
utas_server.py gains FUT_PORT/FUT_LOG so a throwaway instance can be started without
bouncing the one the live client is using. Defaults unchanged (8099, /tmp/utas_server.log).
Noted for the record: this edit came from a research agent that had been told not to
touch server code. It is benign and useful, but it was out of scope.
Not committed: the doc proposes ENDPOINT_MAP.md changes as pasteable text rather than
applying them, and every proposed server change defaults off per the house rule.
Nothing in this commit changes a response the client sees.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
d0dbfa99c0 |
fifa17-recon: the /settings gate bytes were never zero, and the plan built on that is dead
Yesterday's settings-gate plan asserted that IS_FRIENDLY_SEASON_ENABLED and
IS_DRAFT_MODE_ENABLED "have never been set to true by anything, on any run", and
proposed spending a launch on that premise. Measured against the running client,
both read 1, and so does packOpeningAnimationEnabled, while /settings has only ever
been answered {"configs": []}.
disp 0x1fd3a (friendlySeasonsEnabled) value = 1
disp 0x1fd3d (enableDraftMode) value = 1
disp 0x1fd45 (packOpeningAnimationEnabled) value = 1
Reproduced on two separate launches and two different pids.
Where the reasoning went wrong: the finding that FUN_18011dc50 is the only writer of
those bytes, and that the FutDataManagerImpl constructor never touches them, was
correct. The inference was not. The applier runs whether or not the configs array has
content, and the struct it is handed defaults these fields to 1, so the bytes were
being written all along. "Nothing populates the array" was treated as "nothing writes
the byte". Only the first of those was ever established.
Seasons therefore does not refuse because its gate byte is false. Its gate byte is
true. That diagnosis restarts, and the live test in section 3 should not be run as
written. The doc keeps the wrong turn on the record rather than quietly deleting it.
tools/gate_byte_probe.py makes this repeatable instead of a one-off. It is read-only
(O_RDONLY + pread), resolves the pid by comm, re-derives the CardsDLL slide from
/proc/<pid>/maps rather than caching it across launches, proves the slide against the
FNV prologue at 0x180180d00 read from the on-disk PE before trusting any address, and
decodes each gate displacement out of its accessor stub (0f b6 81 <disp32>) rather
than reading it from a table. Needs the client at the FUT hub, since CardsDLL loads
only then.
Also carries the two /settings changes that were pending from before: the mode
defaults to `off` (the live-proven baseline, since nothing here has faced the game)
and the transfer-pile probe is 77 rather than 100, because 100 is a stock-looking
number that would prove nothing if it showed up in game.
Live: 439 contract checks pass. check_settings_flags.py passes in all four modes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
897259c8fb |
fifa17-recon: the /settings 42-flag gate, and why Seasons never asks
ENDPOINT_MAP said this class reads one key, `configs`, and that was true and useless. What it missed is what happens after each element closes: the client feeds the STRING VALUE of `type` back through the atom hasher and switches on the result, 42 arms wide. A flag is a row, not a key, and the client hashes our string itself. Followed it to the end. FUN_18011dc50 is the only writer of the IS_* UI gate bytes inside FutDataManagerImpl, every line is `byte = (field == 1)`, and the constructor never touches those bytes. So a flag nobody sends is a gate nobody opens. friendlySeasonsEnabled and enableDraftMode have never been sent by anything, which is a mechanism for Seasons refusing while making zero requests to any of the four servers. The store is the control that makes this readable: IS_STORE_ENABLED is the same kind of byte and its screen works, because storeEnabled and friends already ship through the Blaze config store. That list has no seasons or draft flag. Ship the gates behind FUT_SETTINGS (off/keep/gates, default gates), and re-assert the working store flags in the same array on purpose: once a populated array makes the applier run, it writes EVERY gate byte, so omitting them could switch off a screen that works today. maximumTradePileSize=100 rides along as a positive control, because a boolean that changes nothing cannot distinguish "the flag did not help" from "the array never reached the consumer". check_settings_flags.py asserts each shipped name against the atom table AND the recovered switch, since a misnamed flag is silently inert and looks exactly like a failed fix. enableSquadBuildingSetsFeature is the reason both checks are needed: a real atom with no arm here. Live: 439 contract checks pass, market unit suite passes. Not yet tested in game. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
9348b83374 |
fifa17-recon: club-item research -- cardtype map exact, itemState carries equipped state
Researched rather than guessed, after a guessed field crashed the client. VERIFIED: FUN_1800d8330 returns cardtype 9 for exactly 0x1e, 0x1f, 0x91..0x96, 0xe7..0xe9, 0xec; fcc_misccards' cardsubtype 231 anchors the 0xe7 block to misc, so badges/kits/stadia/balls/logos live in 0x1e, 0x1f and 0x91..0x96. VERIFIED, and it answers a question nobody had asked: the itemState enum at 0x180229d20 is WAITING_FOR_GAME, inGame, forSale, offered, activeBadge, activeHomeKit, activeAwayKit, activeBall, activeStadium, active. An EQUIPPED club item is the same item with itemState set, not a different subtype. 'free' is right for owned-but-not- equipped, which is what we already send. VERIFIED: club items have no category group table (consumables and staff both do), and the route is club?type= with SINGULAR names, observed live. STILL UNKNOWN and labelled so: which subtype means which family. Not in any of the 149 dumped tables, no group table, and cardtype 9 has no merge arm so a wrong value cannot announce itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
ccf912c157 |
fifa17-recon: club items crashed the client -- unestablished fields, and too wide a blast radius
The game hung and then crashed on the first equippables fetch. My fault twice over.
CAUSE, primary. I copied teamid, leagueid and value straight out of the fcc row as
extras. `value` appears elsewhere as an OBJECT member (displayGroup {"value": ...}),
and a scalar where an object is expected is the type-desync busy loop at 0x1801c7f1a
-- which presents exactly as "the game is taking its time" and then dies. Omission is
safe; an unestablished field is not. That is this project's own rule and I broke it
for three fields that were not needed to draw a card. All three are gone.
CAUSE, contributing. The last request before the crash was type=equippables&count=11
and we answered with 30 items spanning FIVE unverified cardsubtypeids at once: the
widest possible blast radius for a wrong shape, and it tells you nothing about which
subtype was wrong. equippables now answers [] until the subtypes are confirmed one
family at a time, and shelf() takes a families= filter so a test can serve exactly one.
Adds FUT_CLUBITEMS=probe:<family>, which serves one item per candidate subtype for a
single family, so the screen names the correct subtype instead of me guessing a third
time. Eight items, one family, one question.
The flag already defaulted off, so a plain restart cannot serve any of this.
439 + 414 checks green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
f5002a0d4d |
fifa17-recon: serve club items -- the type names are SINGULAR and they were on the wire
Arming the counters made the client name the route within seconds, exactly as it did
for consumables:
club?type=equippables&count=11
club?type=stadium&count=200
club?type=ball&count=200
So it IS club?type= for this family, with SINGULAR names -- stadium and ball, not
stadia and balls -- and equippables as the combined club-customisation view. Those
requests were being answered from STORE.items(), which holds no club items because
the shelf is synthetic, so they correctly returned empty and looked like nothing was
happening.
Now serves the shelf: stadium 4, ball 6, badge 8, kit 8, equippables 30 (all four
combined), with player untouched at 205.
The cardsubtypeid values remain UNVERIFIED. cardtype 9 has no arm in the merge, so a
wrong subtype cannot announce itself; whether these render is now a live question and
the next screenshot answers it.
439 + 414 checks green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
3b58b29094 |
fifa17-recon: club-item counts, and packs that contain more than footballers
Correcting an overstatement first: I said every card family works. Balls, stadia,
badges and kits do not, and this is the start of that, not the finish.
CLUB ITEMS (FUT_CLUBITEMS=1, default off). tools/fut_clubitems.py builds shelves from
the game's own tables: fcc_balls 42, fcc_stadium 78, fcc_badgecards 656,
fcc_kitcards 1482, fcc_leaguelogos 44, each with its real carddbid AND its real
cardassetid. Counts are wired into the club stat set, replacing the honest zeros
rather than appending a second row per id (the deserializer does
store[ctx][statId] = value, so two rows for one id is a coin toss).
Counts FIRST and on purpose. The consumables round proved the count gates the fetch:
the client does not ask for an item list until club/stats reports a non-zero count,
and the CLUB tab reads 0x1e balls, 0x28 kits, 0x14 stadia. Arming the counters is what
makes the client name the item route, which is the one thing static reading has never
produced for this family -- cardtype 9 has NO arm in the merge, so nothing about it is
discoverable from the card DB.
What is deliberately NOT guessed: which cardsubtypeid means ball versus stadium. The
eight values that reach cardtype 9 are {30,31,145..150} and the assignment appears in
none of the 149 dumped tables. probe_shelf() serves one item per candidate so the
screen can say which is which, rather than shipping a guess into someone's club.
PACKS (FUT_PACK_MIX=1, default on). A pack is no longer eleven footballers: roughly a
quarter of each pack is now consumables and occasionally staff, as a ratio so it
scales from a 5-card bronze to an 11-card premium. Club items are excluded until their
subtypes are verified -- a pack is the worst place to discover a wrong subtype,
because the card lands in the save and has to be cleaned out by hand.
Also fixes the art id AT THE SOURCE. fut_consumables now sets cardassetid from the
fcc_ tables, so a pack-granted consumable renders correctly too. The earlier fix only
corrected the copy served by the club route, which meant packs would have dealt cards
with the green NOT FOUND box again.
Pack tests run against a COPY of the profile, after an earlier test persisted 15 cards
into the live save and had to be undone by hand.
439 + 414 checks green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
cef1e8d1f4 |
fifa17-recon: consumable artwork CONFIRMED FIXED, and the two wrong guesses recorded
Real card art, tier colours and the GK glove icon all draw once cardassetid carries the art id. Records both refuted hypotheses with the evidence that killed them, since each was plausible and someone will reach for them again. The generalisable trap: an fcc_ row has BOTH carddbid and cardassetid, they are not interchangeable, and _item copies resourceId into cardassetid -- right for players, wrong for every other family. Club items will hit it next: balls 37, kits 35, stadium 36, badges 39, league logos 40. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
214be11202 |
fifa17-recon: the green NOT FOUND box was a wrong art id, cardassetid != carddbid
A player photographed a green tag under every consumable card. It is not a status label at all: external/ion_fut/artAssets/.../notfound.swf is the client's placeholder for an art asset it could not resolve, so the card was drawing 'no such artwork'. The cause is two id columns that look interchangeable and are not. In the game's own fcc_ tables a consumable row carries BOTH carddbid (5003001) and cardassetid (3), and the art is keyed on the small one. fut_store._item copies resourceId into cardassetid, which is correct for players and wrong for every other family, so the client looked up art id 5003001, found nothing, and fell back. Now mapped from data/tables/fcc_*.json, dumped read-only from the client's own database, so these are the game's ids rather than a guess: training 3, contract 7, healing 10, misc 45. The same table also gives balls 37, kits 35, stadium 36, badges 39, league logos 40, which is what the club-item family will need. Two earlier guesses at this badge were wrong and are recorded as such: it is not the untradeable flag (changing it left the tag untouched, and the record showed the flag had flipped) and not a loc-string failure. 439 + 414 checks green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
c41b1514d3 |
fifa17-recon: consumables are served tradeable, so the untradeable badge clears
A player pointed at a green tag under every consumable card. It is ours, not the client's: the deserializer sets a flag from (untradeableCount < count), we set untradeableCount == count, so every stack read as fully untradeable and drew the badge. UNTRADEABLE_COUNT and UNTRADABLE_COUNT are UI state keys in .rdata. In FIFA 17 a pack-opened consumable is normally tradeable, so this was our own data showing through rather than a rendering fault. Now untradeableCount is 0 and the served copy carries untradeable false. FUT_CONSUM_UNTRADEABLE=1 restores the old behaviour. TODO/CONFIRM: the exact badge text was not read, only its source. If the tag survives this change it is a different label and the untradeable theory is wrong. 439 + 414 checks green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
550313362c |
fifa17-recon: consumables CONFIRMED LIVE -- artwork, stacks and correct amounts
Rendering with real artwork, quantity badges and +5/+10/+15 amounts, so atom 0x1b reaches record+0xbf. The client's own dialog names the class we resolved: 'Search Type: Consumables Search'. Records the three things that each had to be right and each failed silently with a 200: the count gates the fetch, the route is club/consumables/<category> (a /club PREFIX, so it was being answered with the player list), and the element is a five-atom stack wrapper whose 0x16a member is the only thing that carries the item. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
122b7b94d2 |
fifa17-recon: consumables are STACK records, not bare items
The route was right and the body was wrong. We served bare items, the client took the
response and inserted NOTHING (the live card map held only the 11 squad players), and
the screen stayed empty with no error logged anywhere. A 200 with a well-formed body
that the consumer silently discards is the worst failure shape there is, and it is the
third time this project has hit it.
FutConsumablesSearchServerResponse resolved from the RS4 literal 0x1802222f8:
factory 0x180130a10, vtable 0x180222200, deserializer +0x08 = 0x180130d10 (6873
chars). It takes itemData(0x16b) at the root like the club list, but its ELEMENT is
not an item. It is a five-atom wrapper and exactly one of the five carries the item:
0xbc count int
0xd7 discardValue int
0x16a item -> FUN_18013fe00, the item parser itself
0x287 resourceId int
0x362 untradeableCount int
Everything else goes to the value-skip handler, which is precisely why a bare item was
accepted and did nothing. It also explains the UI: FUT draws consumables as one stack
with a quantity, not as N cards, and the wrapper is that quantity.
Identical consumables are now collapsed by resourceId and counted.
439 + 414 checks green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
ccb736fa71 |
fifa17-recon: the consumables item route, found live -- GET club/consumables/<category>
The counter WAS the gate, and fixing it produced the request within seconds:
11:23:36 GET /ut/game/fifa17/club/consumables/training
11:23:40 GET /ut/game/fifa17/club/consumables/contracts
Neither is club?type=, and neither is the /consumables/%s template we had been
hunting in the binary. The client asks here, and it asks ONLY once
club/stats/consumables reports a non-zero count. Two rounds of item-shape work went
unrequested for want of a counter.
Worse, the path is a /club prefix, so it fell through to the generic route: the
consumables screen was answered with the 194-card PLAYER list. It asked for training
cards and got Cristiano Ronaldo.
Now routed above the generic /club, serving the shelf filtered by category. The
segment names come from the UI group table at 0x180203260; training and contracts are
CONFIRMED on the wire, the other five are from that table and matched case
insensitively, with singular "contract" accepted because the client has used both.
An unknown segment serves the whole shelf and logs loudly rather than showing an empty
screen, since a new spelling is a wire fact worth catching.
Per-category counts match the on-screen panel exactly: training 42, contracts 13,
fitness 6, healing 21, position 20, playStyle 24, managerLeague 0. That correspondence
is what makes an empty list distinguishable from a wrong mapping.
439 + 414 checks green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
2f8a6512db |
fifa17-recon: answer the CONSUMABLES panel with consumable counts, not player counts
The tab was empty because we answered the wrong question. The client asks GET club/stats/consumables 41 times a session; we replied with the PLAYER stat set (players 205, playersGold 189 ...), which that panel does not read. Confirmed on screen: seven categories present and selectable, every one reading 0. Now appends 14 consumables* rows counted from the SHELF. The shelf, not STORE.items(): the consumables we serve are a synthetic overlay never granted into the save, so counting the store gives fourteen zeros, which on screen is byte-identical to failure and would have made the experiment unreadable. Counts match the independently derived expectation exactly: 126 total, 21 healing, 7 player contracts, 21 player training, 3 player fitness, 20 position, 21 GK training, 6 manager contracts, 19 playstyle. Safe by construction: the vocabulary is an ATOM switch (FUN_18012fd40, 40 arms, default return 0), so an unrecognised name is inert rather than fatal, and the rows are APPENDED -- the player, nation and league rows that drive the working screens are untouched. Zero rows unless FUT_CONSUMABLES is armed. Default ON because answering the consumables panel with player counts is wrong by inspection rather than a judgement call. FUT_CONSUM_STATS=0 reverts. 439 + 414 checks green. The open question this sets up: whether a non-zero count makes the client request an item list at all. If it does, the log names the route and /consumables/%s is settled for free. If the numbers move and no request follows, the panel renders from counts alone and the 126-item shelf was never needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
0f83d73364 |
fifa17-recon: the consumables panel asks 41 times a session and we answer with players
Round 3, 10 agents. The headline is measured, not inferred: GET club/stats/consumables
is requested 41 times per session by the real client (ProtoHttp), and _club_stat_set()
answers it with the PLAYER stat set. The panel reads 14 consumables* names that we
have never sent, so it is told '205 players' when it asked how many contracts the club
owns, and it has nothing to show.
That also explains why last round's 126-item consumable shelf was never requested. It
serves type=contract|training|healing|development and an UNTYPED /club with no
team=/league=, and all 9 of the client's untyped requests this session carry team=. The
only +126 item(s) line in the whole log came from one of our own probes.
Vocabulary recovered: the 14 consumables* rows plus badgeDBid 0x2e, kitsHome 0x29,
kitsAway 0x2a, leagueLogos 0x2f, trophiesSeasonOnline 0x38.
Other measured surfaces the client asks for and we fob off: GET /settings 11x answered
with an empty config array (a 40-flag feature gate, the biggest untouched lever in the
project), leaderboards/options 5x with {}, user/accountinfo 4x with {}.
club/stats/staff is a DIFFERENT class (FutStaffBonus); the staff counts come from the
Stats2 store, which is why the staff screen worked while we answered {}.
Refuted: ENDPOINT_MAP's claim that objectives have no route. FUN_180151610 builds
<base>/objective/%d/reward and FUN_180147780 builds .../complete.
New modules only. utas_server.py is deliberately untouched: whether to wire the counts
depends on a free observation the human can make on the client that is already running,
and spending a restart before that is what this round exists to avoid.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
456ec24360 |
fifa17-recon: managers and coaches CONFIRMED LIVE, and the manager template paints our fields
34 staff cards, zero DB Error. Every coach id hit its table first time, which is the payoff from enumerating the game's own database instead of sweeping for ids: the loud miss-fill exists and never fired. 10 of 10 managers resolved with historically correct nations and leagues. RESOLVED, previously TODO/CONFIRM: the manager card template paints record+0xde (nation) and record+0xe0 (league). Luis Enrique draws the Spain flag and 'LaLiga Santander'; the Premier League managers draw their flag and 'ENG 1'. The merge never writes either field, so nothing but our own JSON could have supplied them. Corrected: negotiation at +0xe3 is NOT on the card front, which shows CONTRACT 7 there. I had told the user to look for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
22ba361578 |
fifa17-recon: repair_club keeps dead cards unless asked, plus the 2026-08-05 plan
Deleting cards from someone's club is their call, not the tool's. The nine unrepairable blanks are now KEPT unless --delete-dead is passed. A blank card is ugly, not harmful, and the 175 stale cards were never the deletion candidates anyway: they are real players wearing old invented numbers and they get repaired in place. Also records the build round's synthesis as docs/plan-2026-08-05-families.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
21b2263e30 |
fifa17-recon: family flags -- FUT_CONSUMABLES=0 meant ON
os.environ.get returns the STRING "0", which is truthy, so anyone typing
FUT_CONSUMABLES=0 to turn the family off would have turned it on. "", "0", "off",
"no" and "false" now all mean off; any other value is the mode string ("1", "all").
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
0e4bc13db4 |
fifa17-recon: serve consumables, coaches and managers behind flags, default OFF
Wires the three families into club_route as a synthetic OVERLAY. Nothing changes by
default: with all three flags unset every route returns byte-identical bodies, checked
against the live 194-item save (club 194, type=player 194, league=53 drill-down 67,
hub clubPlayers 194, every staff stat 0).
FUT_CONSUMABLES=1 serve on type=contract|training|healing|development
FUT_CONSUMABLES=all ... and on an untyped club fetch with no team=/league=
FUT_COACHES=1 serve on type=headcoach|gkcoach|physio|fitnesscoach|staff
FUT_COACHES=all ... and on type=manager, the one staff request ever observed
FUT_MANAGERS=1 serve on type=manager|staff
WHY AN OVERLAY AND NOT A GRANT. Clearing the flag restores the real club exactly on
the next fetch, with no un-granting and no edit to a save a live client is holding
open. And Store.add_items() uses setdefault("id", ...), so items arriving with an id
already set do NOT advance nextItemId -- granting these would eventually collide two
id spaces. The four overlay bases (9.4e8 consumables, 9.5e8 coaches, 9.6e8 managers,
9.1e8 probe) are asserted disjoint from each other, from the save's 1e8 and from the
sweep's 9e8. The cost is that overlay cards cannot be quick-sold or moved.
WHY EACH FLAG IS TWO-VALUED. The tab-to-?type= binding is UNOBSERVED -- only
type=player, type=manager and type=custom have ever come from this client, and none of
the nine other arm names in FUN_18012ec50 has. So every club fetch now LOGS the arm it
was asked for while any flag is set. That is what makes an empty tab actionable: it
says whether the request reached us and under which name, instead of nothing.
THE MIRROR FILTER, and it is not optional. club_route applied its cardsubtypeid filter
only when `kind and kind not in ("player","custom")`. For type=player, for type=custom
(the by-league / by-team drill-downs) and for an untyped fetch it filtered NOTHING, so
the moment the club held a non-player item it would be served straight into the
players tab and the drill-downs -- and a manager carries nation, leagueId and teamid,
so he would have appeared as a footballer in exactly the MY CLUB rows that were only
just made non-zero. The player branch now filters cardsubtypeid in (0,1,2,3). Provable
no-op today: all 194 items in the save are cardsubtypeid 0.
Same hardening for the three counters that keyed on itemType == "player" (hub
clubPlayers, _club_stat_context, _club_stat_set) -> _is_player(), i.e. the CLIENT's own
definition from FUN_1800d8330. itemType is INERT on the wire (atom 0x173 is parsed into
a stack std::string in FUN_18013fe00 and freed; it never reaches the record), so keying
our own screens on it made their correctness depend on a field the client ignores.
FREE SECOND ORACLE: the five staff sub-type counters (0xb..0xf) are now computed from
the overlay. FUN_180094ce0's STAFF_EMPLOYED row is the +0x800 SUM over those five, so
the parent id 0xa alone could never move it. That number moves without the merge being
involved at all, which keeps "our club reports N staff" separable from "the client
resolved the card".
item_def() also learns consumables (gated): answering a consumable resourceId with
cardsubtypeid 0 makes it cardtype 0 -- no merge arm, no miss-fill, i.e. plausible
garbage -- and it carried the same hardcoded rareflag 1 as fut_store._item().
tools/test_card_families.py: 414 offline checks over the item BUILDERS. Deliberately a
separate suite -- test_fut_contract.py talks to a live server over HTTP and imports
nothing from the server's own code, which is what lets it certify a future non-Python
implementation; these must run against the working tree without restarting anything.
Each guard was mutation-checked: reverting the 219 fix, or letting coach_item accept a
miss-fill assetid, or letting consumable_item accept a dead zone, each fails the suite.
Suites after: test_fut_contract 439/0, test_match_rewards 61/0, test_card_families
414/0. utas_server was NOT restarted; this lands on disk only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
9b22435421 |
fifa17-recon: manager cards -- 417 ids, real names, and a nation/league slot that is ours alone
managercards: 417 rows, carddbid 1000001..1001552, assetid == carddbid on all 417,
value 57..88 (120 rows at exactly 57), rare 282/135 and NOT a rating threshold.
talkrating and formationid are 0 on ALL 417 rows -- both columns are dead in FIFA 17.
MEASURED from data/tables/, rowcount == rows_emitted.
Names come out after all, and by the CLIENT's own rule rather than an inference:
FUN_1801bb060 (879 bytes, read in full) does SELECT firstname,surname,... FROM manager
WHERE managerid == (*(u32*)(rec+0x18) & 0xffffff) - 1000000. Joining data/tables/
manager.json on that rule gives 1000509 Luis Enrique 88, 1000089 Wenger 86, 1000417
Guardiola 87, 1000414 Klopp 84 -- the ratings match the men, which a shifted column
could not produce. This supersedes the note that we get ids and not names: true of
managercards, false once you join `manager`.
Fixed here: `manager` has 747 rows but 746 distinct managerids -- managerid 107 is
duplicated with an empty-name row, and a plain dict comprehension kept the wrong one,
silently giving carddbid 1000107 a blank name and teamid 1357 instead of Slutskiy and
315. 296 -> 297 usable names.
THE KEY FACT, verified at instruction level: the parser routes the JSON `nation` to a
DIFFERENT record offset for a manager. At 0x180140e0b FUN_1800d8330's result is DEC'd
twice -- cardtype 1 stores nation to rec+0x148, cardtype 2 to rec+0xde, everything
else DISCARDS it. leagueId (atom 0x18a) lands unconditionally at rec+0xe0. The manager
merge FUN_1801356c0 (452 bytes, 1,587 chars, read in full) writes only firstname,
lastname, assetid, rating, talkrating, negotiation and rare -- it never touches
rec+0xde/+0xe0. So for a manager, WE are the only source of nation and league, and
they are read: FUN_1801a8580/FUN_1801a8540 feed FUN_1800e5940 ("ManagerCardBio"),
which publishes NATIONALITY, NATIONALITY_ASSET_ID and LEAGUE_ID.
That merge has NO else-branch, so unlike a coach a wrong manager id is COMPLETELY
SILENT. resourceId must equal carddbid exactly -- the manager arm reads the key raw,
with no & 0xffffff.
Also corrected against the design round's own draft: talkrating and negotiation are
NOT unread. FUN_1800e5940 publishes them as ATTRIB_TEAM_TALKS (rec+0xe2) and
ATTRIB_CONTRACT_NEGOTIATION (rec+0xe3). They come from the DB, not from us, and since
talkrating is 0 on all 417 rows TEAM TALKS reads 0 on every manager card in the game.
Not wired into the server in this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|