fifa17: price quick-sell from the client's own discard table
Quick-sell paid an invented five-tier rating ladder (its own comment said "PLACEHOLDER, not EA-authentic"). It was blind to card type and rareflag, so a 94-rated TOTW special and a 94-rated gold common both sold for 1500, and every non-player -- whose Core overall is 0 -- sold for the flat 150 floor. The ladder existed in three places (adapter wire, host payout, an integration test's private copy), which is a drift waiting to happen. Add openfut-adapter-fifa17::fut::discard: the client's own fcc_discardcoins table and its formula, round_half_up(rating * price / 100), keyed (cardtype, level, rare). All of it is already reversed in plan-2026-08-05-store-subsystem.md 3.6 and was verified there against 22 live club items, 22 of 22 exact. DISCARD_COINS is generated from fifa17-recon/data/tables/fcc_discardcoins.json and a test re-reads that file and asserts row-for-row agreement, so the transcription cannot drift. Collapse the three ladders into one method. ItemIdentityResolver::discard_value both stamps the wire discardValue and prices the sale, because a non-zero discardValue suppresses the client's local computation -- whatever is sent is what the player is promised. The host's quick_sell_value is deleted and the integration test's copy now calls the single implementation. A test with a resolver double returning an impossible price proves the credit follows the wire; reverting the payout to a ladder fails it. Gated on OPENFUT_FIFA17_DISCARD_TABLE=1, default off: switching revalues the real 1991-item club 10.5x (1,820,400 -> 19,128,955 coins if wholly liquidated), up for specials and DOWN for consumables, which the ladder overpaid 5.5x. That is an operator's decision. Staff decline to the ladder rather than pay 0: the client re-rates cardtypes 2/3/4/5/10 from its own DB and their rating is not imported. Deliberately not guessed -- see the falsifier in the doc. Verified on staging with the real club, both modes: flag off 1500 wire / 1500 paid; flag on 23760 wire / 23760 paid on an r99 rareflag-11 card (99*24000/100). Consumables price from their catalog rating and agree with the client's own computation. Adapter 244 lib tests, host 121 lib + 45 host_test, fmt and clippy clean.
This commit is contained in:
@@ -208,6 +208,25 @@ pub trait ItemIdentityResolver {
|
||||
None
|
||||
}
|
||||
|
||||
/// The card's discard (quick-sell) value in coins — BOTH the number the
|
||||
/// client displays on the card and the number the server MUST credit when
|
||||
/// it is sold. One method serves both so the wire and the wallet cannot
|
||||
/// disagree: a non-zero `discardValue` suppresses the client's own local
|
||||
/// computation (`0x180141025`), so whatever is sent here is what the player
|
||||
/// is promised.
|
||||
///
|
||||
/// NON-MINTING by contract, like [`Self::subtype_of`] — it takes the Core
|
||||
/// item, never a resolved [`Fifa17Identity`], so pricing a card on a read
|
||||
/// path cannot allocate a wire id.
|
||||
///
|
||||
/// The default is the [`legacy_discard_value`] placeholder ladder, which
|
||||
/// preserves the behaviour of every resolver without a catalog behind it.
|
||||
/// The catalog-backed FIFA17 resolver overrides it with the client's own
|
||||
/// `fcc_discardcoins` table (see [`super::discard`]).
|
||||
fn discard_value(&self, item: &CoreOwnedItem) -> i64 {
|
||||
legacy_discard_value(item.rating)
|
||||
}
|
||||
|
||||
/// The FIFA `cardsubtypeid` of a Core item's definition, or `0` when unknown
|
||||
/// or a player. NON-MINTING by contract: `/club`'s per-family filters call it
|
||||
/// for every owned row, so allocating a wire id here would pollute the
|
||||
@@ -245,9 +264,17 @@ pub struct ShapeStats {
|
||||
pub excluded_non_player: usize,
|
||||
}
|
||||
|
||||
/// Quick-sell / discard value by rating tier (mirrors Core's quick-sell table;
|
||||
/// non-fatal display field).
|
||||
fn discard_value(rating: u8) -> i64 {
|
||||
/// The ORIGINAL rating-only quick-sell ladder. **A placeholder, not
|
||||
/// EA-authentic**: it is blind to both card type and `rareflag`, so it prices a
|
||||
/// 94-rated TOTW special and a 94-rated gold common identically, and it pays a
|
||||
/// flat floor for every non-player (whose Core rating is 0).
|
||||
///
|
||||
/// The client's real value is `round_half_up(rating * fcc_discardcoins.price /
|
||||
/// 100)` — see [`super::discard`], which reproduces it exactly. This ladder is
|
||||
/// retained as the [`ItemIdentityResolver::discard_value`] default so a resolver
|
||||
/// with no catalog behind it keeps its existing behaviour, and so the deployed
|
||||
/// economy only changes when an operator opts in.
|
||||
pub fn legacy_discard_value(rating: u8) -> i64 {
|
||||
match rating {
|
||||
r if r >= 85 => 1500,
|
||||
r if r >= 80 => 900,
|
||||
@@ -271,6 +298,7 @@ pub fn shape_item(
|
||||
item: &CoreOwnedItem,
|
||||
id: Fifa17Identity,
|
||||
ent: &impl ReverseEntityResolver,
|
||||
discard_value: i64,
|
||||
) -> Value {
|
||||
let asset = id.asset_id;
|
||||
let league_id = ent.league_id(&item.league).unwrap_or(0);
|
||||
@@ -308,7 +336,7 @@ pub fn shape_item(
|
||||
"untradeable": false,
|
||||
"contract": 7,
|
||||
"fitness": 99,
|
||||
"discardValue": discard_value(item.rating),
|
||||
"discardValue": discard_value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -533,6 +561,7 @@ mod tests {
|
||||
rareflag: 1,
|
||||
},
|
||||
&ent,
|
||||
legacy_discard_value(86),
|
||||
);
|
||||
assert_eq!(it["id"], 100000001, "wire instance id");
|
||||
assert_eq!(it["resourceId"], 20801);
|
||||
@@ -566,6 +595,7 @@ mod tests {
|
||||
rareflag: 1,
|
||||
},
|
||||
&ent,
|
||||
legacy_discard_value(84),
|
||||
);
|
||||
let b = shape_item(
|
||||
&item("oc-b", "fifa17_101490", 84, "ST"),
|
||||
@@ -576,6 +606,7 @@ mod tests {
|
||||
rareflag: 1,
|
||||
},
|
||||
&ent,
|
||||
legacy_discard_value(84),
|
||||
);
|
||||
assert_eq!(
|
||||
a["resourceId"], b["resourceId"],
|
||||
@@ -604,6 +635,7 @@ mod tests {
|
||||
rareflag: 3,
|
||||
},
|
||||
&ent,
|
||||
legacy_discard_value(92),
|
||||
);
|
||||
assert_eq!(
|
||||
it["resourceId"], 117617092,
|
||||
@@ -708,6 +740,7 @@ mod tests {
|
||||
rareflag: 1,
|
||||
},
|
||||
&ent,
|
||||
legacy_discard_value(86),
|
||||
);
|
||||
emitted.push(player["itemState"].as_str().unwrap().to_string());
|
||||
let staff = shape_staff_item(
|
||||
|
||||
Reference in New Issue
Block a user