feat(fifa17): real player-contract consumable apply, replacing the probe
POST /ut/game/fifa17/item/resource/<rid> {"apply":[{"id":N}]} now performs a
durable atomic contract application instead of falling through to Python.
THE RULE. grant = fcc_contractcards[card][tier(TARGET.rating)], then
min(99, contract + grant). The column is keyed on the TARGET's tier, NOT the
card's own -- all 36 cells of EA's shipped table match the published FIFA 17
matrix, and staging discriminates the two readings outright: a bronze-RARE
card on a rating-89 player granted 3 (the gold column), where the card-level
reading predicts 15.
No client binary reads fcc_contractcards -- a string scan of every .exe/.dll
in the install finds it referenced nowhere, and CardsDLL reads only 14 fcc_
tables (fcc_discardcoins among them, which is why quick-sell prices locally).
Consumable effects are server-authoritative, so EA's shipped table is the only
non-invented source and the client renders whatever we persist and re-serve.
The host computes the grant, Core owns the mutation -- the same split
quick-sell already uses (host prices via discard_value, Core performs
sell_item), and what migration 0027 means by "Core defines NO per-category
formula".
FAILS CLOSED, never 200-and-do-nothing: manager contracts 409 because staff
ratings are unimported so the target tier is unknowable; every other family
409 as unproven; batch 400; unresolvable operand 404. Core's deterministic
refusals pass through with their own status instead of collapsing to 503,
which would tell the client to retry a request that can never succeed.
`contract: 7` stops being a hardcode in shape_item/shape_staff_item and
becomes the fallback for an instance Core tracks no contract for. `fitness: 99`
is the same class of hardcode and is deliberately untouched.
CLEAN CUTOVER: Route::ConsumableApplyProbe, its handler, apply_probe_enabled,
the OPENFUT_FIFA17_APPLY_PROBE gate and both probe scripts are deleted. A
handler no classifier can reach is this repo's recurring defect class, and the
new economy arm preempts the probe. fifa17-migration-rehearse.py also drove
the probe (spelled "apply probe", so an apply-probe grep missed it) and would
have eaten a card off the rehearsal profile; retargeted to a non-mutating
assertion.
Not implemented, on purpose: the stored-manager bonus (real mechanic, rule
appears in no shipped table -- guessing it would corrupt the proven part) and
contract decrement per match (nothing spends contracts yet).
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::fut::content_taxonomy::ContentKind;
|
||||
use crate::fut::contract_cards::PACK_FRESH_CONTRACT_MATCHES;
|
||||
use crate::fut::entities::ReverseEntityResolver;
|
||||
use crate::fut::item::{shape_club_item, shape_item, shape_staff_item, STAFF_CONTRACT};
|
||||
use crate::fut::item_state;
|
||||
@@ -54,7 +55,13 @@ pub fn shape_club_response_with_kits<I: ItemIdentityResolver + ?Sized>(
|
||||
match ident.kind_of(item) {
|
||||
ContentKind::Player => match ident.resolve(item) {
|
||||
Some(id) => {
|
||||
out.push(shape_item(item, id, ent, ident.discard_value(item)));
|
||||
out.push(shape_item(
|
||||
item,
|
||||
id,
|
||||
ent,
|
||||
ident.discard_value(item),
|
||||
item.contract_matches.unwrap_or(PACK_FRESH_CONTRACT_MATCHES),
|
||||
));
|
||||
stats.emitted += 1;
|
||||
}
|
||||
None => stats.dropped_no_asset += 1,
|
||||
@@ -83,7 +90,10 @@ pub fn shape_club_response_with_kits<I: ItemIdentityResolver + ?Sized>(
|
||||
// because a guard arm would not prove exhaustiveness).
|
||||
ContentKind::Manager | ContentKind::Staff => match ident.resolve_staff(item) {
|
||||
Some(id) => {
|
||||
out.push(shape_staff_item(id, STAFF_CONTRACT));
|
||||
out.push(shape_staff_item(
|
||||
id,
|
||||
item.contract_matches.unwrap_or(STAFF_CONTRACT),
|
||||
));
|
||||
stats.emitted += 1;
|
||||
}
|
||||
None => stats.dropped_no_asset += 1,
|
||||
@@ -116,6 +126,7 @@ pub fn shape_club_response_with_kits<I: ItemIdentityResolver + ?Sized>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::fut::contract_cards::CONTRACT_MATCH_CAP;
|
||||
use crate::fut::entities::Fifa17Entities;
|
||||
use std::collections::HashMap;
|
||||
|
||||
@@ -145,6 +156,9 @@ mod tests {
|
||||
league: league.into(),
|
||||
club: club.into(),
|
||||
attributes: [90, 88, 70, 85, 40, 78],
|
||||
// Untracked by default, so these fixtures exercise the pack-fresh
|
||||
// fallback; a test that cares sets it explicitly.
|
||||
contract_matches: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,7 +375,11 @@ mod tests {
|
||||
assert_eq!(coach["resourceId"], 3000083);
|
||||
assert_eq!(coach["cardsubtypeid"], 8);
|
||||
assert_eq!(coach["itemType"], "staff");
|
||||
assert_eq!(coach["contract"], STAFF_CONTRACT);
|
||||
assert_eq!(
|
||||
coach["contract"], STAFF_CONTRACT,
|
||||
"this fixture is UNTRACKED (contract_matches None), so the wire shows \
|
||||
the pack-fresh fallback — not because the shaper hardcodes it"
|
||||
);
|
||||
assert!(
|
||||
coach.get("nation").is_none()
|
||||
&& coach.get("leagueId").is_none()
|
||||
@@ -411,7 +429,10 @@ mod tests {
|
||||
assert_eq!(mgr["nation"], 45);
|
||||
assert_eq!(mgr["leagueId"], 53);
|
||||
assert_eq!(mgr["teamid"], 241);
|
||||
assert_eq!(mgr["contract"], STAFF_CONTRACT);
|
||||
assert_eq!(
|
||||
mgr["contract"], STAFF_CONTRACT,
|
||||
"untracked fixture => pack-fresh fallback"
|
||||
);
|
||||
assert_eq!(mgr["itemState"], "free");
|
||||
assert_eq!(mgr["owners"], 1);
|
||||
let keys: Vec<&String> = mgr.as_object().unwrap().keys().collect();
|
||||
@@ -422,6 +443,78 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// `/club` is the screen a contract apply is judged on: if the envelope keeps
|
||||
/// reporting the pack-fresh count, a committed apply is invisible and the
|
||||
/// operator sees a 200 that did nothing. Both families must carry the number
|
||||
/// Core persisted.
|
||||
#[test]
|
||||
fn club_reports_the_contract_core_persisted_for_players_and_staff() {
|
||||
let ent = entities();
|
||||
let ident = KindMapIdentity {
|
||||
ids: HashMap::from([(
|
||||
"card_player".to_string(),
|
||||
Fifa17Identity {
|
||||
item_id: 100000001,
|
||||
asset_id: 20801,
|
||||
resource_id: 20801,
|
||||
rareflag: 1,
|
||||
},
|
||||
)]),
|
||||
kits: HashMap::new(),
|
||||
staff: HashMap::from([(
|
||||
"card_manager".to_string(),
|
||||
Fifa17StaffIdentity {
|
||||
item_id: 100004871,
|
||||
resource_id: 1000509,
|
||||
subtype: 4,
|
||||
nation: 45,
|
||||
league_id: 53,
|
||||
team_id: 241,
|
||||
},
|
||||
)]),
|
||||
kinds: HashMap::from([
|
||||
("card_player".to_string(), ContentKind::Player),
|
||||
("card_manager".to_string(), ContentKind::Staff),
|
||||
]),
|
||||
};
|
||||
// A player mid-way through its contracts, a fully topped-up manager, and
|
||||
// one untracked player that must fall back.
|
||||
let mut played = item(
|
||||
"oc-played",
|
||||
"card_player",
|
||||
86,
|
||||
"ST",
|
||||
"Argentina",
|
||||
"Premier League",
|
||||
"Chelsea",
|
||||
);
|
||||
played.contract_matches = Some(3);
|
||||
let mut manager = item("oc-mgr", "card_manager", 0, "", "", "", "");
|
||||
manager.contract_matches = Some(CONTRACT_MATCH_CAP);
|
||||
let untracked = item(
|
||||
"oc-fresh",
|
||||
"card_player",
|
||||
86,
|
||||
"ST",
|
||||
"Argentina",
|
||||
"Premier League",
|
||||
"Chelsea",
|
||||
);
|
||||
|
||||
let (body, stats) = shape_club_response(&[played, manager, untracked], &ent, &ident);
|
||||
assert_eq!(stats.emitted, 3);
|
||||
let arr = body["itemData"].as_array().unwrap();
|
||||
assert_eq!(arr[0]["contract"], 3, "the player's persisted count");
|
||||
assert_eq!(
|
||||
arr[1]["contract"], CONTRACT_MATCH_CAP,
|
||||
"staff read the same persisted field, not STAFF_CONTRACT"
|
||||
);
|
||||
assert_eq!(
|
||||
arr[2]["contract"], PACK_FRESH_CONTRACT_MATCHES,
|
||||
"only an untracked instance falls back"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kits_project_with_owned_active_home_and_away_states() {
|
||||
let ent = entities();
|
||||
|
||||
Reference in New Issue
Block a user