6c97bc4e2b
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).
115 lines
4.8 KiB
Bash
Executable File
115 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# FIFA 17 squad-adapter mutation battery.
|
|
#
|
|
# Each mutation injects a specific WRONG behaviour into the committed source,
|
|
# runs the one test that defends the invariant, and requires that test to FAIL
|
|
# (non-zero exit) — i.e. the mutant is killed. The source is reverted via
|
|
# `git checkout` after every mutation, so the tree is left untouched.
|
|
#
|
|
# A mutant that SURVIVES (its guard test still passes) means the invariant is
|
|
# not actually defended; the battery then exits non-zero.
|
|
#
|
|
# Run from the adapter crate root: bash mutation-battery.sh
|
|
set -u
|
|
cd "$(dirname "$0")"
|
|
|
|
FUT=src/fut
|
|
PASS=0
|
|
FAIL=0
|
|
declare -a SURVIVORS=()
|
|
|
|
# mutate <file> <literal-old> <literal-new> (literal, multiline-safe)
|
|
mutate() {
|
|
OLD="$2" NEW="$3" perl -0777 -pi -e \
|
|
's/\Q$ENV{OLD}\E/$ENV{NEW}/g or die "MUTATION PATTERN NOT FOUND in '"$1"'\n"' "$1"
|
|
}
|
|
|
|
# kill <n> <label> <test-filter> <file> <old> <new>
|
|
kill_test() {
|
|
local n="$1" label="$2" filter="$3" file="$4" old="$5" new="$6"
|
|
git checkout -- "$file"
|
|
mutate "$file" "$old" "$new" || { echo " [#$n] SETUP ERROR"; FAIL=$((FAIL+1)); SURVIVORS+=("#$n $label (setup)"); return; }
|
|
if cargo test --quiet "$filter" >/dev/null 2>&1; then
|
|
echo " [#$n] SURVIVED — $label (test '$filter' still passed)"
|
|
FAIL=$((FAIL+1)); SURVIVORS+=("#$n $label")
|
|
else
|
|
echo " [#$n] killed — $label"
|
|
PASS=$((PASS+1))
|
|
fi
|
|
git checkout -- "$file"
|
|
}
|
|
|
|
echo "== FIFA17 squad adapter mutation battery =="
|
|
|
|
kill_test 1 "kitNumber keyed by slot index, not owned item" \
|
|
kit_number_is_keyed_by_owned_item_not_slot "$FUT/squad_ext.rs" \
|
|
'(s.owned_card_id.clone(), s.kit_number)' '(s.index.to_string(), s.kit_number)'
|
|
|
|
kill_test 2 "captain emitted using resourceId (asset), not wire id" \
|
|
fresh_projects_full_23_slot_array_with_captain_wire_id "$FUT/squad_projection.rs" \
|
|
'captain_wire = id.item_id as i64;' 'captain_wire = id.asset_id as i64;'
|
|
|
|
kill_test 3 "client chemistry silently reconciled (shadow lost)" \
|
|
swap_moves_two_players_with_their_kits_and_round_trips "$FUT/squad_projection.rs" \
|
|
'"chemistry": ext.client_reported.chemistry,' '"chemistry": 0,'
|
|
|
|
kill_test 4 "custom[] regenerated instead of round-tripped verbatim" \
|
|
custom_is_preserved_byte_for_byte "$FUT/squad_ext.rs" \
|
|
'custom: put.custom.clone(),' 'custom: Some("[]".to_string()),'
|
|
|
|
kill_test 5 "index derived (zeroed) instead of round-tripped" \
|
|
baseline_projects_the_known_squad_round_trip "$FUT/squad.rs" \
|
|
'index: p.index,' 'index: 0,'
|
|
|
|
kill_test 6 "stale extension accepted and projected" \
|
|
stale_is_never_applied "$FUT/squad_projection.rs" \
|
|
'SquadExtInput::Stale(_) => return Ok(SquadProjection::Stale),' 'SquadExtInput::Stale(ext) => ext,'
|
|
|
|
kill_test 7 "missing extension fabricates default fields" \
|
|
missing_is_explicit_never_fabricated "$FUT/squad_projection.rs" \
|
|
'SquadExtInput::Missing => return Ok(SquadProjection::Missing),' \
|
|
'SquadExtInput::Missing => return Ok(SquadProjection::Projected(json!({"custom":"[]","manager":[]}))),'
|
|
|
|
kill_test 8 "shaper fabricates asset id, bypassing real FIFA identity" \
|
|
shapes_real_identity_and_reverse_entity_ids "$FUT/item.rs" \
|
|
'let asset = id.asset_id;' 'let asset = 0;'
|
|
|
|
kill_test 9 "duplicate definition collapses owned instances (id=asset)" \
|
|
two_owned_copies_of_one_definition_stay_distinct_on_the_wire "$FUT/item.rs" \
|
|
'"id": id.item_id,' '"id": id.asset_id,'
|
|
|
|
kill_test 10 "PUT treated as a partial slot diff (drops slots)" \
|
|
full_replacement_carries_every_occupied_slot_no_diff "$FUT/squad.rs" \
|
|
'if p.item_data.id == 0 {' 'if p.item_data.id == 0 || p.index > 1 {'
|
|
|
|
kill_test 11 "FIFA wire item id stored in canonical replacement" \
|
|
full_replacement_carries_every_occupied_slot_no_diff "$FUT/squad.rs" \
|
|
'ProposedSlot {
|
|
owned_card_id,' 'ProposedSlot {
|
|
owned_card_id: p.item_data.id.to_string(),'
|
|
|
|
kill_test 12 "projector rebuilds items independently of shared shaper" \
|
|
persisted_read_round_trips_via_reconstructed_canonical_and_extension "$FUT/squad_projection.rs" \
|
|
'"itemData": shape_item(
|
|
item,
|
|
id,
|
|
ent,
|
|
ident.discard_value(item),
|
|
item.contract_matches.unwrap_or(PACK_FRESH_CONTRACT_MATCHES),
|
|
),' '"itemData": json!({"id": id.item_id}),'
|
|
|
|
kill_test 13 "extension schema version ignored on read" \
|
|
unknown_schema_version_is_rejected_not_coerced "$FUT/squad_ext.rs" \
|
|
'if schema_version != EXT_SCHEMA_VERSION {' 'if false {'
|
|
|
|
kill_test 14 "player state keyed by CardDefinitionId not OwnedItemId" \
|
|
two_owned_copies_of_one_definition_project_as_distinct_players "$FUT/squad_projection.rs" \
|
|
'.get(&slot.owned_card_id)' '.get(&item.card_id)'
|
|
|
|
echo "== mutants killed: $PASS / $((PASS+FAIL)) =="
|
|
if [ "$FAIL" -ne 0 ]; then
|
|
printf 'SURVIVORS:\n'; printf ' - %s\n' "${SURVIVORS[@]}"
|
|
exit 1
|
|
fi
|
|
echo "all mutants killed"
|