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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user