diff --git a/fifa17-recon/tools/cardsdll_kit_strings.py b/fifa17-recon/tools/cardsdll_kit_strings.py new file mode 100755 index 0000000..b978f87 --- /dev/null +++ b/fifa17-recon/tools/cardsdll_kit_strings.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Recover the kit caption/localisation vocabulary from the UNPACKED CardsDLL. + +Why CardsDLL and not FIFA17.exe: CardsDLL is not packed, so a MISS here is +meaningful. FIFA17.exe is Denuvo-packed and only partially readable -- a hit +there is useful, a miss proves nothing. Every run therefore prints a positive +control first; if the control fails, the run is void and no negative may be +quoted from it. + +Usage: python3 cardsdll_kit_strings.py [path-to-CardsDLL] +""" +from __future__ import annotations + +import os +import re +import sys + +DEFAULT = os.path.expanduser( + "~/.cache/openfut-investigation/bin/CardsDLL_Win64_retail.dll" +) + +# Strings that MUST be present. If any is missing the search is broken. +CONTROLS = [b"activeHomeKit", b"cardsubtypeid", b"resourceId", b"activeAwayKit"] + +# The kit caption vocabulary this project has referred to, plus neighbours worth +# knowing about either way. +PROBES = [ + b"FUT_UC_KITS", b"TeamName_Abbr15_", b"TeamName_Abbr15", b"TeamName_", + b"FUT_UC_", b"StadiumName_", b"Badge", b"Stadium", + b"activeBadge", b"activeBall", b"activeStadium", + b"kit", b"Kit", b"KIT", + b"home", b"Home", b"HOME", b"away", b"Away", b"AWAY", + b"locked", b"Locked", b"LOCKED", b"unlock", + b"category", b"year", b"teamid", b"teamId", + b"DataProvider", b"itemData", b"itemType", b"itemState", +] + + +def ascii_strings(data, minlen=4): + for m in re.finditer(rb"[ -~]{%d,}" % minlen, data): + yield m.start(), m.group() + + +def main(): + path = sys.argv[1] if len(sys.argv) > 1 else DEFAULT + data = open(path, "rb").read() + print(f"{os.path.basename(path)} {len(data)} bytes") + + print("\n-- positive control (a miss voids every negative below) --") + ok = True + for c in CONTROLS: + n = data.count(c) + print(f" {c.decode():16s} {n}") + if n == 0: + ok = False + if not ok: + print(" CONTROL FAILED — do not quote negatives from this run.") + return 1 + + print("\n-- probe counts --") + for p in PROBES: + print(f" {p.decode():18s} {data.count(p)}") + + # Whole-string table: every standalone string containing kit-ish substrings. + print("\n-- standalone strings matching kit/team/caption vocabulary --") + pat = re.compile(rb"(?i)(kit|teamname|abbr|stadiumname|fut_uc|locked|unlock)") + seen = set() + for off, s in ascii_strings(data, 5): + if pat.search(s) and s not in seen: + seen.add(s) + print(f" @{off:#08x} {s.decode('latin1')[:110]}") + print(f" ({len(seen)} distinct)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/openfut-adapter-fifa17/src/fut/item.rs b/openfut-adapter-fifa17/src/fut/item.rs index 37f2a1d..7052ae3 100644 --- a/openfut-adapter-fifa17/src/fut/item.rs +++ b/openfut-adapter-fifa17/src/fut/item.rs @@ -371,38 +371,35 @@ pub fn shape_item( /// Wire `itemType` (atom 0x173) for a cardtype-7 club item. /// -/// MEASURED 2026-08-23, live client pid 8793 parked on the pre-match kit -/// selector, read-only `/proc/PID/mem`. Whether a served club item becomes a -/// resident item record correlates perfectly with whether we send `itemType`: +/// Sent for WIRE FIDELITY only. Every real EA item in the capture corpus carries +/// `itemType`, and the two families OpenFUT already shaped (`player`, `staff`) +/// carry it, so omitting it on the club families was an inconsistency. Tokens +/// come from the `?type=` vocabulary decoded from the `FUN_18012ec50` jump table +/// (`kit` 12, `stadium` 13, `badge` 11). /// -/// ```text -/// family itemType sent resident record? -/// player "player" yes -/// staff "staff" yes -/// kit (absent) NO -/// badge (absent) NO -/// stadium (absent) NO -/// ``` +/// It does NOT fix the pre-match kit selector, and the reasoning that first +/// introduced it was WRONG. That reasoning was: kit/badge/stadium omitted +/// `itemType` and were not resident as item records, while player and staff sent +/// it and were, so `itemType` must gate ingestion. Adding it changed nothing — +/// the client was relaunched, `?type=kit` answered `total=2 emitted=2` with +/// `itemType` present, and still no cardtype-7 record was resident. /// -/// Two of two families that carry it are ingested; none of the three that omit -/// it is. With no cardtype-7 record resident, the club scan `FUN_1800d73d0` -/// (`+0x4c == 7 && +0x50 == 9 && +0x5c in {101,102}`) matches nothing, the FUT -/// match-kit DataProvider is built empty (traced: `KITS_AVAILABLE = 0`), and the -/// selector falls back to catalogue-gated engine kits — which is the observed -/// "This kit is currently locked" dialog. +/// The correlation was an artefact of the CONTROL, not the field. Measured +/// 2026-08-23 read-only over `/proc/PID/mem`: the "resident" players and staff +/// were all SQUAD members, which arrive via `userMassInfo`. Testing players that +/// appear in `/club?type=player` but NOT in `userMassInfo` shows they are not +/// resident either — 0 records for 6 of 6 sampled, 5 with no byte match at all, +/// out of 1966 served. So residency tracks the ROUTE, not this field: +/// `/club?type=` responses do not enter the persistent card collection, and no +/// value of `itemType` changes that. /// -/// `CARD_SYSTEM.md` records that `itemType` "is parsed into a heap string and -/// never stored". That remains true of the RECORD; it does not follow that the -/// string is unused, and the correlation above is evidence that it is consulted -/// before the record is retained. +/// `CARD_SYSTEM.md`'s "parsed into a heap string and never stored" therefore +/// stands unchallenged; the earlier note here that it was "evidence the string is +/// consulted" is withdrawn. /// -/// The tokens are the `?type=` vocabulary decoded from the `FUN_18012ec50` jump -/// table (`kit` 12, `stadium` 13, `badge` 11), which is the same vocabulary the -/// two working families already use (`player` 1, `staff` 10). -/// -/// INFERRED, not proven: no capture of a real EA club item exists anywhere in -/// the corpus, so the exact token for these three families is taken from the -/// atom vocabulary rather than observed on the wire. +/// INFERRED, not proven: no capture of a real EA club item exists anywhere in the +/// corpus, so the exact token for these three families is taken from the atom +/// vocabulary rather than observed on the wire. fn club_item_type(subtype: i64) -> &'static str { match subtype { KIT_SUBTYPE => "kit", @@ -1099,10 +1096,7 @@ mod tests { year: 0, }; let type_of = |subtype| { - shape_club_item( - Fifa17KitIdentity { subtype, ..base }, - item_state::FREE, - )["itemType"] + shape_club_item(Fifa17KitIdentity { subtype, ..base }, item_state::FREE)["itemType"] .as_str() .expect("itemType is always emitted") .to_string() diff --git a/openfut-utas-host/src/lib.rs b/openfut-utas-host/src/lib.rs index 6e594e1..c72278b 100644 --- a/openfut-utas-host/src/lib.rs +++ b/openfut-utas-host/src/lib.rs @@ -2382,73 +2382,6 @@ fn club_type_filter(token: Option<&str>) -> Option { Some(filter) } -/// DIAGNOSTIC, `OPENFUT_FIFA17_KIT_PROBE=1`, staging only, default OFF. -/// -/// Appends two synthetic kits to `?type=kit` so one client restart can settle -/// which of the two remaining kit-ingest hypotheses is right. It answers a -/// question no static reading has: the client is served two kits, returns -/// `total=2 emitted=2`, and yet NO cardtype-7 record is ever resident (measured -/// read-only over 3.6 GB of process memory, while player and staff records from -/// the same response family ARE resident). -/// -/// Two shapes, because two things could be rejecting the item: -/// -/// * `6300007` MINIMAL — exactly the field set a STAFF item carries, which is -/// known to ingest, plus `cardsubtypeid` 9. If only this one appears, one of -/// the kit-only extras (`assetId`, `cardassetid`, `teamid`, `category`, -/// `year`) is what makes the client discard the item. -/// * `6300008` NAMED — the full kit shape plus `name`/`localizedName`/ -/// `description`. The cardtype-7 parse arm is documented to copy exactly those -/// three, and OpenFUT sends none of them. If only this one appears, they are -/// required rather than optional. -/// -/// If NEITHER appears, `?type=kit` is not the route that populates the -/// collection `FUN_1800d73d0` scans, and the search moves to which route does. -/// -/// Both ids are real `fcc_kitcards` carddbids for team 21 (`6300007` year 1972 -/// category 2, `6300008` year 0 category 5), so nothing here invents an id the -/// client cannot resolve. They are served `free`, never active, so they cannot -/// disturb the real active-kit assignment. Instance ids are outside Core's -/// range so they can never collide with an owned row. -/// -/// Deliberately small: the one response that has ever crashed this client was 30 -/// items across five families (2026-08-05). This is two items in one family. -fn append_kit_shape_probe(label: &str, items: &mut Vec) { - if label != "kit" || !kit_probe_enabled() { - return; - } - items.push(json!({ - "id": 100009007, - "resourceId": 6300007, - "cardsubtypeid": 9, - "itemType": "kit", - "itemState": "free", - "owners": 1, - "untradeable": false, - })); - items.push(json!({ - "id": 100009008, - "resourceId": 6300008, - "assetId": 6300008, - "cardassetid": 35, - "cardsubtypeid": 9, - "itemType": "kit", - "itemState": "free", - "owners": 1, - "untradeable": false, - "teamid": 21, - "category": 5, - "year": 0, - "name": "OpenFUT Probe Kit", - "localizedName": "OpenFUT Probe Kit", - "description": "kit ingest probe", - })); - eprintln!( - "utas-host owner=RUST route=club KIT_PROBE armed: appended 6300007 (minimal) \ - and 6300008 (named) — diagnostic only" - ); -} - /// Filter already-shaped `/club` items to SPECIALS (`rareflag > 1`) and paginate /// the filtered set locally. Returns `(page, total_specials)`. Pure — the whole /// point is that "special" pagination is over the filtered set, never Core's @@ -2615,12 +2548,11 @@ pub fn handle_club(query: &str, deps: &ClubDeps<'_>) -> (WireResponse, ClubLog) }; let (body, stats) = shape_club_response_with_kits(&visible, deps.entities, deps.assets, active); - let mut all = body + let all = body .get("itemData") .and_then(Value::as_array) .cloned() .unwrap_or_default(); - append_kit_shape_probe(filter_arm.label, &mut all); let (paged, total) = if core_q.special { special_filter_page(&all, offset, limit) } else { @@ -5669,15 +5601,6 @@ fn equippables_enabled() -> bool { *ENABLED.get_or_init(|| std::env::var("OPENFUT_FIFA17_EQUIPPABLES").as_deref() == Ok("1")) } -/// Append the two diagnostic kits described on [`append_kit_shape_probe`]. -/// -/// Default OFF, staging only. Flip the env var off to revert with a restart and -/// no rebuild. -fn kit_probe_enabled() -> bool { - static ENABLED: std::sync::OnceLock = std::sync::OnceLock::new(); - *ENABLED.get_or_init(|| std::env::var("OPENFUT_FIFA17_KIT_PROBE").as_deref() == Ok("1")) -} - /// A JSON response with an explicit status. fn json_status(status: u16, v: &Value) -> WireResponse { let body = serde_json::to_vec(v).unwrap_or_default();