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:
funman300
2026-08-22 18:23:22 +00:00
parent 3c67fea074
commit 6c97bc4e2b
19 changed files with 1348 additions and 270 deletions
@@ -35,6 +35,7 @@ use std::collections::HashMap;
use serde_json::{json, Value};
use crate::fut::contract_cards::PACK_FRESH_CONTRACT_MATCHES;
use crate::fut::entities::ReverseEntityResolver;
use crate::fut::item::{
shape_item, shape_staff_item, CoreOwnedItem, ItemIdentityResolver, STAFF_CONTRACT,
@@ -166,7 +167,13 @@ pub fn project_squad<I: ItemIdentityResolver + ?Sized>(
.unwrap_or(0);
players.push(json!({
"index": index,
"itemData": shape_item(item, id, ent, ident.discard_value(item)),
"itemData": shape_item(
item,
id,
ent,
ident.discard_value(item),
item.contract_matches.unwrap_or(PACK_FRESH_CONTRACT_MATCHES),
),
"kitNumber": kit,
}));
}
@@ -199,10 +206,14 @@ pub fn project_squad<I: ItemIdentityResolver + ?Sized>(
// An owned manager with no resolvable FIFA staff identity is omitted
// (non-fatal, like /club dropping an unrenderable card) rather than emitted
// with a fabricated id.
let manager = match input.manager.as_ref().and_then(|m| ident.resolve_staff(m)) {
Some(id) => json!([{
let manager = match input
.manager
.as_ref()
.and_then(|m| ident.resolve_staff(m).map(|id| (m, id)))
{
Some((mgr, id)) => json!([{
"id": id.item_id,
"itemData": shape_staff_item(id, STAFF_CONTRACT),
"itemData": shape_staff_item(id, mgr.contract_matches.unwrap_or(STAFF_CONTRACT)),
"dream": false,
}]),
None => json!([]),
@@ -287,6 +298,7 @@ mod tests {
league: "l".into(),
club: "c".into(),
attributes: [80, 80, 80, 80, 40, 80],
contract_matches: None,
}
}
@@ -502,7 +514,12 @@ mod tests {
)]),
);
let mut input = one_slot_input(&owned, SquadExtInput::Fresh(fresh_ext()));
input.manager = Some(owned_item("oc-mgr", "fifa17_mgr"));
// A manager mid-way through its contracts: the projection must report
// Core's persisted count, not the pack-fresh constant, or the pre-match
// screen contradicts the club screen.
let mut manager = owned_item("oc-mgr", "fifa17_mgr");
manager.contract_matches = Some(12);
input.manager = Some(manager);
let SquadProjection::Projected(v) = project_squad(&input, &ident, &ent()).unwrap() else {
panic!("expected Projected");
};
@@ -521,7 +538,7 @@ mod tests {
"nation": 45,
"leagueId": 53,
"teamid": 241,
"contract": STAFF_CONTRACT,
"contract": 12,
"itemState": "free",
"owners": 1,
"untradeable": false,