From d8ef9d4c4f20ac0538c378bf8240a3e0a5440312 Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 5 Aug 2026 10:03:54 -0700 Subject: [PATCH] fifa17-recon: the rareflag trap -- a rare Player Fitness card is a SQUAD Fitness card fut_store._item() hardcoded "rareflag": 1 on every item it builds. That is inert for players and for staff, and CORRUPTING for exactly one consumable subtype. MEASURED, in the binary: FUN_1801bfac0 case 5 (consumable category 5, fitness) takes the squad-fitness branch when (cardsubtypeid == 0xdc) || FUN_1801a88c0(rec) and FUN_1801a88c0 is exactly `*(int *)(rec + 0x58) == 1`. rec+0x58 is the rareflag atom 0x271 (FUN_18013fe00 case 0x271 -> uStack_130; the frame arithmetic is independently pinned by local_138 -> rec+0x50 and local_13c -> rec+0x4c, the two offsets card_identity_probe has been reading live for days). FUN_180141660 does not overwrite rec+0x58 for cardtype 6 -- cases 6/7/8/9 fall to the shared tail, which writes only rec+0x54 -- so a rareflag we send survives all the way to the render. Result: subtype 219 with rareflag 1 draws FUT_CONSUMABLE_NAME_SQUADTRAINING with artwork 5000011 instead of Player Fitness with 5000010, and forces the single-target count at param_5+0x1bc to 0. Silent. It would have corrupted the first fitness card we ever served. The guard is `0 if cardsubtypeid == 219 else rareflag`, added with two new KEYWORD params. Every existing call site (fut_store.py:74/:357, utas_server.py:1404/:2136) passes 8 positional args, so both take their defaults and the player dict is byte-identical -- key order included, asserted in tools/test_card_families.py. Scope correction to the round's own notes: rec+0x58 is read TWICE in that 42,813-char render function, not once. FUN_1801a88c0 is the category-5 read, but line 108 reads it directly into param_5+0x1f0 (the rare/backing art) for EVERY cardtype, before the `if (param_4 == 6)`. So the guard also stops a 219 being drawn as rare -- intended, since rare IS the squad-fitness selector. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW --- fifa17-recon/tools/fut_store.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/fifa17-recon/tools/fut_store.py b/fifa17-recon/tools/fut_store.py index 455d48e..d05cc0d 100644 --- a/fifa17-recon/tools/fut_store.py +++ b/fifa17-recon/tools/fut_store.py @@ -44,16 +44,35 @@ STARTER_PLAYERS = [ ITEM_ID_BASE = 100000000 -def _item(item_id, asset, rating, pos, nation, league, team, attrs, version=0x00): +# cardsubtypeid 219 is Player Fitness. FUN_1801bfac0 case 5 (consumable category 5) +# takes the SQUAD-fitness branch when `(subtype == 0xdc) || FUN_1801a88c0(rec)`, and +# FUN_1801a88c0 is exactly `*(int *)(rec + 0x58) == 1` -- rec+0x58 being the rareflag +# atom 0x271. So a Player Fitness card sent with rareflag 1 silently RENDERS as a +# Squad Fitness card (name FUT_CONSUMABLE_NAME_SQUADTRAINING, artwork 5000011 instead +# of 5000010) and has its single-target count at param_5+0x1bc forced to 0. +# +# rec+0x58 is read TWICE in that 42,813-char render function: unconditionally near the +# top into param_5+0x1f0 (the rare/backing flag, every cardtype), and via FUN_1801a88c0 +# in category 5 only. So this guard changes two things for subtype 219 -- the card also +# stops being drawn as rare -- and that is intended: a Player Fitness card must not be +# rare, because rare IS the squad-fitness selector. +# +# Players are untouched: every existing caller passes 8 positional arguments, so +# cardsubtypeid defaults to 0, 0 != 219, and the dict is byte-identical to before. +_SQUAD_FITNESS_TRAP = 219 + + +def _item(item_id, asset, rating, pos, nation, league, team, attrs, version=0x00, + cardsubtypeid=0, rareflag=1): return { "id": item_id, "resourceId": (version << 24) | asset, "assetId": asset, "cardassetid": asset, "definitionId": (version << 24) | asset, - "cardsubtypeid": 0, + "cardsubtypeid": cardsubtypeid, "itemType": "player", - "rareflag": 1, + "rareflag": 0 if cardsubtypeid == _SQUAD_FITNESS_TRAP else rareflag, "rating": rating, "preferredPosition": pos, "nation": nation,