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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
This commit is contained in:
funman300
2026-08-05 10:03:54 -07:00
parent 9876a6c870
commit d8ef9d4c4f
+22 -3
View File
@@ -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,