d929efdffe8c104f413dd6aabb0d7295ca76fe5b
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
743b20b00b |
kits: 0x757a is the command "useSavedMatchKits" - the gate was never the bug
Recovered from the live retail client (pid 44405, parked on the pre-match kit
selector) by read-only /proc/PID/mem. No writes, no debugger, no detours, no
client modification. Denuvo decrypts FIFA17.exe in memory, so the immediate that
is absent on disk is present at runtime.
At live 0x147de80e8 FIFA17.exe runs a name-registration loop through the same
string-setter vtable slot (+0x20) CardsDLL uses, so each id can be given its
name:
0x7579 useSavedMatchData -> movb $1, ctx+0x151
0x757a useSavedMatchKits -> movb $1, ctx+0x152 == KITS_AVAILABLE
0x7587 setFUTServerEnvironment
271 pairs recovered and committed. This is a DIFFERENT namespace from CardsDLL's
DataProvider id table -- the same numeric id has a different name in each --
matching the APT's own split between game.uif.UIFDataProviderList and the
action/command list.
KITS_AVAILABLE therefore does not mean "the server sent kits". It means "use the
previously SAVED match kits", and the APT writes those via ACTION_SAVE_MATCH_KIT
in SaveKitsForMatch. Live, both flags read 0 on a fresh match at the exact
moment the tiles are blank. So 0 is CORRECT on first entry and the client is
designed to take the fallback path. The gate was never the defect, which is what
the 2026-08-23 measured negative was already indicating when better item data
changed nothing.
The fallback is GetKitArrayForFUT -> ION_Uniform.GetIDs(teamId), which natively
gates on team id 130000 (0x1800d8ab0) or 130001 (0x1800d8ad0) -- the synthetic
FUT home/away pair -- and otherwise defers to the generic engine catalogue. For
a matching team it packs the two active kit records and admits them through a
check that resolves at runtime to "cmp edx,0x189a1003 / setne al", i.e. merely
"not the invalid sentinel".
Still no OpenFUT change: whether the selector requests 130000/130001, and
whether the active kit records resolve, are both unanswered and both live
questions. Full write-up in the Vault under Kit Selector APT Decode section 8.
|
||
|
|
5bf2c7ddc1 |
kits: the pre-match kit screen is futSelectTeam, found without Frosty
The Frostbite .cas chunks holding APT ActionScript are greppable, so screens can
be identified and their whole symbol table recovered without driving the GUI.
Control: KitAssignmentPopup (a string from an already-exported BIG) hits 43 times
across the 52 cas files, so a miss would have been meaningful.
FUT_GET_MATCH_KITS_DP hits 10 times. The binding screen is
external.ion_fut.screens.futSelectTeam
(fifa_installpackage_01/cas_01.cas @ 0x3707ecd7), which no exported BIG contained
after 33 attempts at guessing names.
It binds FUT_GET_MATCH_KITS_DP, KitSelectDP and TeamSetupDP, and carries exactly
the vocabulary the native side implies:
panels/locks mcKitHome mcKitAway mcLockHome mcLockAway m_arrKitPanels
sides HOME_SIDE AWAY_SIDE NEUTRAL_SIDE SIDE_HOME SIDE_AWAY
DP fields KITS_AVAILABLE KIT_ HOME_KIT_ID AWAY_KIT_ID
flow InitializeKitConfig GetKitArrayForFUT InitializeKitsFromArray
EnterKitSelect IsKitSelectCreated ExitKitSelect SaveKitsForMatch
lock CheckIsKitLocked RemoveKitLocks SetKitReady SetKitUnReady
uniform SetUniform ION_Uniform GetNonConflictingUniformID
CheckIsKitLocked is the lock predicate the native side does not own — recall
sub_180033430 only ever writes LOCKED = 0, so the "kit is currently locked" dialog
is raised here.
Adds find_apt_in_cas.py (control-guarded) and the recovered 934-symbol table.
|
||
|
|
eefa98c961 |
kits: canonical table-proven kit map, and category is the home/away key (not the id band)
Answers from the extracted client tables, before touching a binary. Every number is a count over the full table. fcc_kitcards 1482 rows, teamkits 2576 rows. CATEGORY -> ENGINE KIT TYPE, with a test that can actually fail. Asserting "category 3 means away" because away kits usually exist is not evidence: types 0/1/2 are present for most teams, so it is true by construction. The discriminating cases are the teams that LACK a type. category 2 -> type 0 HOME 54 keys lack type 0, 0 counterexamples category 3 -> type 1 AWAY 166 keys lack type 1, 0 counterexamples category 5 -> type 2 THIRD 145 keys lack type 2, 0 counterexamples and there is never more than one card per (team, year, category). THE ID BAND IS NOT HOME/AWAY. Band 6300000 holds 740 HOME cards AND 88 THIRD cards; band 6400000 holds the 654 AWAY cards. assetid is fully determined by the band (14 for 828/828 of 63xxxxx, 15 for 654/654 of 64xxxxx), so it carries no information the band does not. cardassetid is 35 on all 1482 rows - it is the FUT card frame, not the kit art. This matters for openfut-adapter-fifa17: KIT_AWAY_FLOOR splits home from away at 6_400_000, which is right for home vs away but silently classifies all 88 THIRD kits as HOME. Recorded here, not yet fixed - third kits are not currently ownable, so nothing observable depends on it. teamkits.islocked is 0 on all 2576 rows, so the DB lock flag is NOT what makes the pre-match selector call a kit locked. 6 rows are embargoed. Team 21, the staging club, resolves exactly: 6300006 cat 2 HOME year 0 assetid 14 -> teamkitid 1376 6400003 cat 3 AWAY year 0 assetid 15 -> teamkitid 1377 6300007 cat 2 HOME year 1972 assetid 14 -> teamkitid 5126 6300008 cat 5 THIRD year 0 assetid 14 -> teamkitid 1378 so the two kits OpenFUT serves are the correct home/away pair. NOT recoverable from data/tables: the kit's own name string. fcc_kitcards name/header/description/biodescription are byte OFFSETS into the table's string blob (583, 597, 608, 619 on one row), and that blob is not among the extracted tables. Adds audit_fifa17_kits.py (the tool, with the discriminating test inline) and fifa17-kit-map.json (its output) so this is reusable data rather than terminal scrollback. |
||
|
|
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 |
||
|
|
9feb577c1c |
fifa17-recon: the four coach families -- 411 real ids, and a miss that labels itself
headcoachcards 124 rows 2000004..2000328, gkcoachcards 121 rows 9000001..9000324, physiocards 51 rows 4000002..4000259, fitnesscoachcards 115 rows 3000019..3000328. All MEASURED from data/tables/, dumped read-only from the running client; rowcount == rows_emitted == len(rows) on all four, which is what makes "this id is absent" a claim about a complete dump rather than about a truncated one. assetid == carddbid on every row; every id fits in 24 bits. WHY COACHES ARE THE CHEAPEST FAMILY TO TEST. Their four arms of FUN_180141660 (2,129 bytes, 214-line decompile read to its closing `return`) are the only merges in the game that label their own failure: on rowcount < 1 each writes firstname = lastname = "DB Error", rec+0xb4 = 0x32, rec+0x58 = 1 and a TABLE-UNIQUE assetid -- head 2000148, fitness 3000259, physio 4000146, gkcoach 9000258. Two independent facts make that a one-glance oracle, both verified by exhaustive scan of all 411 rows: no row in any of the four tables has value == 50, and no fitnesscoach row is (fieldpos 1, posbonus 7, amount 1). CORRECTION to docs/plan-2026-08-04-card-families.md: the miss-fill is NOT uniform. Only head coach and GK coach write 0xf into the attribute array at rec+0x98. Physio writes 0xf into a BYTE at rec+0xdd; fitness coach writes no 0xf at all -- rec+0xde = 0x107 and rec+0xdd = 1. So card_identity_probe's attrs column means something different per family, and its F_NAME_KNOWN=0xdd string read sits directly on top of physio's, fitness coach's and the manager's raw stat bytes. Use coach_probe.py. The key is RAW: all four staff branches pass *(u32*)(rec+0x18) unmasked into `WHERE carddbid == ?`. Players are the only family that masks with & 0xffffff, so a version byte in the top octet breaks every staff lookup -- silently on a manager, loudly on a coach. WHAT WE SEND: id, resourceId, cardsubtypeid, itemType, contract, itemState, owners, untradeable. Nothing else. rating/rareflag/assetId are overwritten by the merge; nation/leagueId/teamid would be INVENTED, because none of the four tables has such a column; preferredPosition (rec+0x146) and attributeList (rec+0x98..) SURVIVE the merge and are read by the generic view-model FUN_1800d7920, so sending them would hang a position label and six attribute numbers on a coach. Omission is safe; a scalar where an object is expected is not. The starter shelf is one card per (tier, rare) combination per family -- 24 cards -- with two exclusions: the four miss-fill assetids (three of which are REAL rows, so a hit and a miss would look identical on those cards), and any row whose own stat write is byte-identical to its family's miss-fill (head/GK attribute 0 amount 15). tier() is the binary's own tail, not our convention: the shared exit of FUN_180141660 writes rec+0x54 = 3 if rating >= 0x4b else 2 - (rating < 0x41), for every arm including the miss arms. Also lands the design round's read-only probe tooling: coach_probe.py (grades a live record HIT/MISS/WRONG-BRANCH/NO-MERGE against the on-disk rows) and coach_window.py (builds a mixed-control window; fires nothing). 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 |
||
|
|
340c31f34e |
fifa17-recon: consumables -- the whole family, and it needs no id space at all
144 live cardtype-6 subtypes and 28 dead zones, all derived from the binary rather
than guessed, plus EA's own authored variants out of the dumped fcc_* tables.
MEASURED (decompiles read to their closing brace, lengths stated):
FUN_1800d8330 (714 chars) cardsubtypeid -> cardtype. The cardtype-6 space is
{51..136} u {201..220} u {250..273} u {300..341} = 172.
FUN_18013f4d0 (8,354 chars) subtype -> category(rec+0xb8), sub-sel(rec+0xbc i16),
amount(rec+0xbf i8), single(rec+0xc0). Two callees, a
range clamp and an enum map; no DB handle is touched,
which is why a consumable has no identity to look up.
FUN_1801bfac0 (42,813 chars) category -> FUT_CONSUMABLE_* string + a HARDCODED
5000xxx artwork constant. resourceId never reaches the
screen for a consumable.
fcc_trainingcards 143 rows / fcc_healingcards 27 / fcc_contractcards 13, each with
rowcount == rows_emitted == len(rows), so absences below are from a COMPLETE dump.
Two things the family will not forgive, both enforced in the builder rather than
documented and hoped for:
* `amount` (atom 0x1b) is MANDATORY for categories 0, 4, 5, 9, 10. The parser
initialises its temp to 0xffffffffffffffff, so omitting it stamps (byte)-1, and
the accessors FUN_1801a8040/FUN_1801a8060 are `(int)*(char *)` -- SIGNED. The
card reads "-1", not 0. consumable_item() raises instead.
* A DEAD-ZONE subtype does not self-label. It falls to the bottom default of
FUN_18013f4d0 and renders as an ordinary Squad Training (Pace) card with amount
0. There is no "DB Error" analogue here, so every subtype we ship comes from
data/consumables.json and the builder refuses the other 28.
Two corrections to the generated data, both from re-reading FUN_1801bfac0 case 5 and
case 0 rather than from the category table:
* subtype 220 is named FUT_CONSUMABLE_NAME_SQUADTRAINING, not ..._PLAYERFITNESS.
0xdc == 220 is the FIRST half of the squad-fitness test, so 220 always takes that
branch, and there is no ..._SQUADFITNESS string in the binary at all.
* all 28 dead zones are SQUADTRAINING, not PLAYERTRAINING: case 0 tests
`subtype - 0x33 < 7` then `subtype - 0x3d < 7` and no dead zone satisfies either.
Exactly 29 of 172 rows changed; nothing else moved.
INFERRED, and flagged as such in the module: the ?type= grouping. The vocabulary is
certain (FUN_18012ec50 arms healing=23, contract=24, training=25, development=6), but
the tab-to-arm binding has NEVER been observed -- only type=player, type=manager and
type=custom have ever come from this client.
Three families deliberately NOT shipped: manager_formation_mod (71-86) has zero rows
in the 143-row table AND FUN_1801bfac0 case 6 calls FUN_1801a0100 on the formations
result without the rowcount guard its twin case 7 has -- a crash candidate;
formation_mod (121-136) has artwork -1; manager_league (300-341) renders literally
"ML: %d" from a raw number and one shipped amount (2118) is in no league table.
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
|
||
|
|
d17cf684ea |
fifa17-recon: keep raw memory captures out of git
data/memdump reached 2.3GB of raw /proc/PID/mem captures. Only its index.json is worth tracking; the captures regenerate from tools/db_dump.py against a running game. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
9a0a76c9f4 |
fifa17-recon: real positions, clubs and the six card attributes for all 17,563 players
The pool now comes from the game's OWN resident database, not from a rating index plus guesses. tools/db_dump.py walked the client's self-describing table catalog read-only and wrote data/tables/ (149 tables, 55MB); tools/build_player_facts.py turned it into data/player_facts.json; data/pool.json is the compact form fut_cards loads. MEASURED, per player: position (players.preferredposition1), nationality, teamid, leagueid (via leagueteamlinks), and the six card attributes. The six attributes are NOT columns -- they are a weighted sum of the 29 base attributes, and the weights are read out of the game's own playerattributesmapping table rather than from published formulas. Checked against real FIFA 17 cards: Messi 89/90/86/96/26/61 and Ibrahimovic 72/90/81/85/31/86 are EXACT, Suarez is one off on physical, Ronaldo within two on pace and shooting. Keepers come out directly from the gk* columns. What this fixes on screen: Kaka was a CDM, Bale a CM, Suarez a GK, and every attribute was derived from the rating. Now Bale is RW, Boateng is a CB with 90 defending, De Gea is a GK, and a bronze pack deals real bronze players in real positions. REVERSAL, deliberate: nation/team/league were being sent as ZERO so the client would fill its own values (the merge fills those three only when they arrive zero). Now that we hold the game's own numbers there is nothing to gain from zeros, and they actively hurt -- club-stats drill-downs bucket by the item's own nation and leagueId, so a club full of zeros would have quietly emptied the per-nation and per-league panels fixed the day before. Send the real values. The old rating-index path is kept as a fallback so the pool still builds without data/pool.json, and it now says out loud which of the two it used, because one is a measurement and the other is a guess. 439 + 61 checks green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW |
||
|
|
e9e6f203c2 |
fifa17-recon: the card pool is now the REAL FIFA 17 roster, 17547 players
The old pool was 79 hand-written rows whose asset ids were mostly invented, on
the premise that the client's card map is empty offline so no id could render.
That premise was refuted by a live screenshot, and this replaces its consequence.
Source: tools/dbdata_extract.py reads FIFA's own rating-sorted index out of a
running process (0x40 stride, self-validating {begin,end,end+1} name-pointer
triple, anchored on 20801 = Ronaldo 94) -> data/roster.json. dbdata.dll was a
dead end and is documented as such: its single export getTableData is an
anti-tamper attestation routine, not a data accessor.
Cross-validated against a completely independent method. tools/sweep_collect.py
serves candidate ids as a synthetic club and reads back the identity the CLIENT
resolved through its own merge. 573 of 573 overlapping names agreed exactly, and
the single id present in one and not the other is 26501, the target of the
documented 22800..22879 Legends remap -- which is also what produced 'Alex Hunter
x80' in a sweep and had looked like a bug.
Field honesty, because half of these are real and half are not:
playerid/rating/name REAL the roster
club/nation/league REAL we send zeros and the CLIENT fills them (the merge
only fills those fields when they arrive as zero)
position PARTLY 59 from the game's own per-card cache, 17 curated
by hand, the rest synthetic but deterministic
attributes SYNTH derived from rating and position
169193 is dropped from the curated set: it was in VERIFIED_ASSET_IDS and is not a
real player. The client resolves it to the database's empty placeholder row, which
renders as 'Jamal Blackman'. Two independent methods agreed.
NOTE BEFORE PUSHING ANYWHERE PUBLIC: data/roster.json is EA's player data,
extracted from your own installation. Fine locally; think twice about publishing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|
||
|
|
b0bbc2a07f |
fifa17-recon: sweep auto-advance + the three-state oracle, live-proven
The oracle is three-valued, and all three fingerprints are now confirmed live
against a running client rather than read out of Ghidra:
NAMED our sentinel rating 7 survives and a real name appears. The id is
real, and teamid/nation/leagueId come back FILLED by the game
because we send them as zero.
placeholder rating 7 survives but the name is 'Jamal Blackman', team 0. The
players-table row exists and is an empty slot. This is the trap:
169193 does this and it was in VERIFIED_ASSET_IDS.
MISS rating 0x32, teamid 0x78d, nation 0xe, position 2, name ' '. That
is the binary's miss-fill, byte for byte, and it is exactly the
blank card photographed in a pack today.
Scale: 5000 candidates per response ingests cleanly; 20000 was served and then
silently not ingested (the map did not change at all), so the ceiling is between
them and auto chunks default to 5000.
Auto-advance: the client PAGES the club, so one visit yields several fetches.
'auto:lo-hi:step' hands out the next chunk per fetch. Item ids derive from the
candidate's offset in the WHOLE range, not its index in the chunk, so chunks never
collide and results accumulate across fetches for a single probe at the end.
sweep_collect.py accumulates into data/players.json and rejects the placeholder
name as a matter of course. Yield in 20000-24999 was 19 real ids per 5000, which
is why auto-advance matters: the real roster clusters in 150000-240000.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
|