feat(fifa17): manager contracts, from a Core-owned staff tier

ROOT CAUSE, one line. openfut-import-fifa17 emitted `"overall": 0` for every
non-player Core definition while `d.rating` already held EA's authoritative
`value` -- and the very next block wrote that same number correctly to the
adapter catalog. So the tier existed host-side but never reached Core:
Core overall 0 -> /collection effective_overall 0 -> CoreOwnedItem.rating 0 ->
tier_for_rating(0) = Bronze for a Gold (88) manager. That silent mis-grant is
exactly what the 409 was protecting against, so the refusal was correct.

The emitter now also writes `source_rating`, keeping `overall` at 0. Regenerating
the production pack changes exactly 18 entries and exactly one field each
(source_rating None -> value); same 1710 ids, same fingerprint 28c333f1e833338a.

WHY value IS the tier source, and why the thresholds are the player ladder:
LIVE_PROVEN, not inferred. The client re-rates staff from its own
managercards/*coachcards/physiocards by carddbid and applies discard_level's
65/75 ladder; coach_probe/discard_probe agree 4/4 (manager value 88 -> level 3,
coaches 66 -> level 2). The shipped coach tables corroborate: each family has
exactly 3 tiers x 2 rarities, and only 65/75 splits them 2/2/2.

Manager contracts stop refusing and now resolve the TARGET's tier from
Core-owned state. Still fail-closed everywhere it matters: a coach or physio is
`contract_target_not_a_manager` (only cardsubtypeid 4 is a manager), and a
manager Core carries no source_rating for is `manager_tier_unknown` rather than
a guessed tier. Core's own content_kind token is sent as target_kind, because
Core calls the squad manager `manager` while the catalog classifies it
`staff`+subtype 4.

NOT implemented, unchanged: STORED_MANAGER_BONUS and MATCH_CONTRACT_DECREMENT.
This commit is contained in:
funman300
2026-08-22 20:08:26 +00:00
parent f0c6dcf238
commit 9026220533
13 changed files with 541 additions and 68 deletions
+6
View File
@@ -110,6 +110,10 @@ fn core_owned(m: &Minted) -> CoreOwnedItem {
// Freshly minted by a pack/Store open, so Core tracks no contract for it
// yet: the shaper substitutes the pack-fresh default.
contract_matches: None,
// A pack mints PLAYER cards, whose rating IS `overall`, so there is no
// separate authored definition rating to carry.
source_rating: None,
core_content_kind: None,
}
}
@@ -1002,6 +1006,8 @@ mod tests {
club: "Arsenal".into(),
attributes: [rating; 6],
contract_matches: None,
source_rating: None,
core_content_kind: None,
}
}
+354 -59
View File
@@ -58,10 +58,11 @@ use openfut_adapter_fifa17::fut::club_response::{
use openfut_adapter_fifa17::fut::club_stats::{club_stats_body, ClubStatInput, ContextField};
use openfut_adapter_fifa17::fut::consumables::consumables_response;
use openfut_adapter_fifa17::fut::content_taxonomy::{
consumable_families_for_category, consumable_family, position_group, ContentKind, PositionGroup,
consumable_families_for_category, consumable_family, position_group, ContentKind,
PositionGroup, MANAGER_SUBTYPE,
};
use openfut_adapter_fifa17::fut::contract_cards::{
contract_grant, tier_for_rating, CONTRACT_MATCH_CAP, MANAGER_CONTRACT_SUBTYPE,
contract_grant, staff_tier, tier_for_rating, CONTRACT_MATCH_CAP, MANAGER_CONTRACT_SUBTYPE,
PACK_FRESH_CONTRACT_MATCHES, PLAYER_CONTRACT_SUBTYPE,
};
use openfut_adapter_fifa17::fut::discard;
@@ -1796,6 +1797,19 @@ fn core_item_from_json(e: &Value) -> Option<CoreOwnedItem> {
attr("physical"),
],
contract_matches: e.get("contract_matches").and_then(|v| v.as_i64()),
// Core's authored definition rating for a NON-PLAYER (a staff card's EA
// `value`). It is a separate key from `overall`, which Core deliberately
// keeps at 0 for non-players because that number feeds pricing.
source_rating: card
.get("source_rating")
.and_then(|v| v.as_i64())
.map(|r| r as u8),
// Core's own kind token, verbatim: Core says `manager` where the FIFA
// catalog says `staff` + subtype 4, and Core compares against its own.
core_content_kind: e
.get("content_kind")
.and_then(|v| v.as_str())
.map(str::to_string),
})
}
@@ -1832,6 +1846,10 @@ fn core_item_from_definition(card: &Value) -> Option<CoreOwnedItem> {
// reasoning as the empty `owned_card_id` above. `None` makes the caller
// substitute the pack-fresh default instead of reading a fabricated 0.
contract_matches: None,
// Likewise: a definition is not an instance, so it carries neither an
// instance-scoped authored rating nor Core's per-instance kind token.
source_rating: None,
core_content_kind: None,
})
}
@@ -4875,10 +4893,14 @@ impl Server {
/// tier, not by the consumable's own tier, and the table is not monotonic, so
/// it can only be looked up ([`contract_grant`]) — never interpolated.
///
/// Only the CONTRACT family is served, and only its PLAYER half (subtype
/// 201). Everything else fails closed. A 200-and-do-nothing here is precisely
/// the defect this route was claimed to end: the Python oracle maps
/// `item/resource` method-agnostically to its definition route, so an
/// Only the CONTRACT family is served, in both halves: subtype 201 to a
/// player, 202 to a manager. Each half takes the TARGET's tier from where
/// that kind's rating actually lives — a player's from Core's `overall`, a
/// manager's from Core's `source_rating` (EA's staff `value`, which the client
/// itself re-rates from) — and refuses when the number is absent rather than
/// defaulting a tier. Everything else fails closed. A 200-and-do-nothing here
/// is precisely the defect this route was claimed to end: the Python oracle
/// maps `item/resource` method-agnostically to its definition route, so an
/// unclaimed apply returns a definition list, consumes nothing, and the
/// client reports success.
///
@@ -4938,40 +4960,23 @@ impl Server {
);
return error_response(404, "not_owned");
};
match source_ident.subtype {
PLAYER_CONTRACT_SUBTYPE => {}
MANAGER_CONTRACT_SUBTYPE => {
// The grant is selected by the TARGET's rating tier, and staff
// ratings are not imported: a manager's rating lives in the
// `value` column of `managercards`/`*coachcards`/`physiocards`
// (see `Fifa17IdentityResolver::discard_value`) and Core models a
// non-player's `overall` as 0. So there is no honest tier for a
// manager target — inventing one would silently grant the wrong
// number of matches, unreversibly. Refuse until staff ratings
// are imported.
eprintln!(
"utas-host owner=RUST route=economy consumable-apply status=409 \
resource={resource_id} wire={target_wire} subtype={} \
outcome=manager_contract_unsupported reason=staff_ratings_not_imported",
source_ident.subtype
);
return error_response(409, "manager_contract_unsupported");
}
other => {
// Only the contract family's effect is proven. Fitness, healing,
// position, play-style and training grants are not, and answering
// 200 while changing nothing is the exact failure this route was
// claimed to end.
eprintln!(
"utas-host owner=RUST route=economy consumable-apply status=409 \
resource={resource_id} wire={target_wire} subtype={other} \
outcome=apply_effect_unproven"
);
return error_response(409, "apply_effect_unproven");
}
// Only the CONTRACT family's effect is proven — in either of its halves.
// Fitness, healing, position, play-style and training grants are not, and
// answering 200 while changing nothing is the exact failure this route was
// claimed to end.
let subtype = source_ident.subtype;
if subtype != PLAYER_CONTRACT_SUBTYPE && subtype != MANAGER_CONTRACT_SUBTYPE {
eprintln!(
"utas-host owner=RUST route=economy consumable-apply status=409 \
resource={resource_id} wire={target_wire} subtype={subtype} \
outcome=apply_effect_unproven"
);
return error_response(409, "apply_effect_unproven");
}
// Reverse the target's wire id through the identity store — never a
// guess, and never the wire id itself.
// guess, and never the wire id itself. Both contract families resolve
// their target the same way; only the legal target KIND and the source of
// its tier differ, so the resolution is shared and the branch is below.
let target = self
.resolver
.owned_id_for_wire(target_wire)
@@ -4988,28 +4993,68 @@ impl Server {
);
return error_response(404, "not_owned");
};
// Subtype 201 is the PLAYER contract; the client's own family gating
// sends manager contracts (202) to staff. A player contract on a
// non-player has no proven effect at all.
let target_kind = self.resolver.kind_of(target_item);
if target_kind != ContentKind::Player {
eprintln!(
"utas-host owner=RUST route=economy consumable-apply status=409 \
resource={resource_id} wire={target_wire} target_kind={} \
outcome=contract_target_not_a_player",
target_kind.as_str()
);
return error_response(409, "contract_target_not_a_player");
}
let Some(granted) = contract_grant(resource_id, tier_for_rating(target_item.rating)) else {
// The subtype said "player contract" while the resource id is not one
// The grant COLUMN is the TARGET's tier, and each family reads it from a
// different place because the two target kinds store their rating
// differently. Gate the legal kind first, then take the tier.
let tier = if subtype == PLAYER_CONTRACT_SUBTYPE {
// Subtype 201 is the PLAYER contract; the client's own family gating
// sends manager contracts (202) to staff. A player contract on a
// non-player has no proven effect at all.
if target_kind != ContentKind::Player {
eprintln!(
"utas-host owner=RUST route=economy consumable-apply status=409 \
resource={resource_id} wire={target_wire} target_kind={} \
outcome=contract_target_not_a_player",
target_kind.as_str()
);
return error_response(409, "contract_target_not_a_player");
}
// A player's rating IS Core's `overall`, so the card ladder reads it.
tier_for_rating(target_item.rating)
} else {
// Subtype 202 is the MANAGER contract, and a MANAGER specifically:
// all five staff families share `ContentKind::Staff`, so the kind
// alone would let a head coach, fitness coach, physio or GK coach
// through. Only `cardsubtypeid` 4 is the squad manager.
let target_subtype = self.resolver.subtype_of(target_item);
if target_subtype != MANAGER_SUBTYPE {
eprintln!(
"utas-host owner=RUST route=economy consumable-apply status=409 \
resource={resource_id} wire={target_wire} target_kind={} \
target_subtype={target_subtype} outcome=contract_target_not_a_manager",
target_kind.as_str()
);
return error_response(409, "contract_target_not_a_manager");
}
// A manager's authoritative rating is EA's `value` column, which Core
// carries as `source_rating`; its `overall` is deliberately 0 for a
// non-player because that number feeds pricing. Reading `overall`
// here would score every gold manager as bronze.
let Some(tier) = staff_tier(target_item.source_rating) else {
// FAIL CLOSED: an un-imported manager has no honest tier, and
// inventing one would silently grant the wrong number of matches,
// irreversibly.
eprintln!(
"utas-host owner=RUST route=economy consumable-apply status=409 \
resource={resource_id} wire={target_wire} target_kind={} \
outcome=manager_tier_unknown reason=core_carries_no_source_rating",
target_kind.as_str()
);
return error_response(409, "manager_tier_unknown");
};
tier
};
let Some(granted) = contract_grant(resource_id, tier) else {
// The subtype said "contract card" while the resource id is not one
// of the 13 known rows: a catalog inconsistency, not a licence to
// substitute a neighbouring row's number.
eprintln!(
"utas-host owner=RUST route=economy consumable-apply status=409 \
resource={resource_id} wire={target_wire} rating={} \
resource={resource_id} wire={target_wire} rating={} tier={} \
outcome=apply_effect_unproven reason=resource_not_a_contract_card",
target_item.rating
target_item.rating,
tier.as_str()
);
return error_response(409, "apply_effect_unproven");
};
@@ -5023,11 +5068,20 @@ impl Server {
"fifa17:apply:{}->{}",
source_item.owned_card_id, target_core_id
);
// Core's `require_kind` compares against ITS OWN token, not the FIFA
// catalog's: Core calls the squad manager `manager` where the catalog says
// `staff` + subtype 4, so sending the catalog kind would make Core refuse
// every manager apply. Use the token Core itself published for this
// instance, falling back to the catalog kind only when Core sent none.
let core_kind = target_item
.core_content_kind
.as_deref()
.unwrap_or_else(|| target_kind.as_str());
let req = ConsumableApplyRequest {
action_identity: &action_identity,
source_owned_card_id: &source_item.owned_card_id,
target_owned_card_id: &target_core_id,
target_kind: target_kind.as_str(),
target_kind: core_kind,
effect: AddContractMatches {
amount: granted,
cap: CONTRACT_MATCH_CAP,
@@ -5064,9 +5118,10 @@ impl Server {
};
eprintln!(
"utas-host owner=RUST route=economy consumable-apply resource={resource_id} \
wire={target_wire} subtype={} granted={} before={} after={} applied={} \
wire={target_wire} subtype={} tier={} granted={} before={} after={} applied={} \
source_destroyed={}",
source_ident.subtype,
tier.as_str(),
outcome.granted,
outcome.before,
outcome.after,
@@ -5589,12 +5644,24 @@ mod tests {
use super::*;
use crate::async_bridge::AsyncBridge;
/// One recorded consumable apply. It captures the three things the caller —
/// not Core — decides: which instance is mutated, the KIND token Core will
/// dispatch on, and the resolved grant. A wrong tier shows up here as a wrong
/// `amount`, which is exactly the silent mis-credit the 202 arm had to avoid.
#[derive(Debug, Clone, PartialEq, Eq)]
struct RecordedApply {
target_owned_card_id: String,
target_kind: String,
amount: i64,
}
/// A configurable in-memory economy double: real balance/entitlements, or a
/// forced error to prove fail-closed behavior.
struct FakeEconomy {
balance: i64,
entitlements: Vec<EconomyEntitlement>,
fail: bool,
applies: PlMutex<Vec<RecordedApply>>,
}
impl FakeEconomy {
fn ok(balance: i64, ents: usize) -> Self {
@@ -5607,12 +5674,14 @@ mod tests {
})
.collect(),
fail: false,
applies: PlMutex::new(Vec::new()),
}
}
fn failing() -> Self {
FakeEconomy {
balance: 0,
entitlements: vec![],
applies: PlMutex::new(Vec::new()),
fail: true,
}
}
@@ -5628,6 +5697,7 @@ mod tests {
})
.collect(),
fail: false,
applies: PlMutex::new(Vec::new()),
}
}
}
@@ -5701,12 +5771,30 @@ mod tests {
coins_balance: self.balance,
})
}
/// Stands in for Core's atomic transaction: it records what the caller
/// asked for and echoes the arithmetic Core would perform, so a test can
/// assert BOTH the refusals (nothing recorded) and the accepted grants.
fn apply_consumable(
&self,
_req: &ConsumableApplyRequest<'_>,
req: &ConsumableApplyRequest<'_>,
) -> Result<ConsumableApplyOutcome, CoreError> {
// Consumable apply is not exercised through this double.
Err(CoreError::Status(501))
if self.fail {
return Err(CoreError::Status(503));
}
self.applies.lock().push(RecordedApply {
target_owned_card_id: req.target_owned_card_id.to_string(),
target_kind: req.target_kind.to_string(),
amount: req.effect.amount,
});
let before = req.effect.default_when_unset;
Ok(ConsumableApplyOutcome {
applied: true,
source_destroyed: true,
source_quantity_after: None,
granted: req.effect.amount,
before,
after: (before + req.effect.amount).min(req.effect.cap),
})
}
fn purchase_item(
&self,
@@ -6100,6 +6188,8 @@ mod tests {
club: "Chelsea".into(),
attributes: [90, 90, 80, 91, 33, 80],
contract_matches: None,
source_rating: None,
core_content_kind: None,
}
}
@@ -6794,6 +6884,211 @@ mod tests {
assert!(parse_apply_targets(br#"{"apply":[{"noid":7}]}"#).is_empty());
}
// ── Consumable apply: the contract family, both halves ───────────────────
/// The apply-path catalog, carrying the same kind+subtype fields the
/// production catalog carries so `resolve_consumable`, `kind_of` and
/// `subtype_of` all answer exactly as they do live. A contract card's
/// `asset_id` IS its `resourceId` (version 0), which is how the shipped
/// `fcc_contractcards` rows are keyed.
///
/// Both contract cards are authored `rating: 66` — SILVER as a card. That is
/// deliberate: it makes the card's own tier differ from a gold target's, so a
/// regression that reads the CARD's tier instead of the TARGET's is caught by
/// the grant number rather than passing silently.
fn apply_catalog() -> Fifa17CardCatalog {
Fifa17CardCatalog::from_json_str(
r#"{"schema_version":1,"game":"fifa17","cards":{
"card_player":{"asset_id":20801},
"card_manager":{"asset_id":1000509,"kind":"staff","subtype":4},
"card_coach":{"asset_id":1000601,"kind":"staff","subtype":5},
"contract_player":{"asset_id":5001002,"kind":"consumable","subtype":201,"rating":66},
"contract_manager":{"asset_id":5001008,"kind":"consumable","subtype":202,"rating":66}
}}"#,
)
.unwrap()
}
/// A NON-PLAYER owned instance as Core actually reports one: `overall` 0
/// (that number feeds pricing, so Core keeps it at 0), the authoritative EA
/// `value` in `source_rating`, and Core's OWN kind token — `manager` for the
/// squad manager, where the FIFA catalog says `staff` + subtype 4.
fn staff_owned(
owned_id: &str,
card: &str,
source_rating: Option<u8>,
core_kind: &str,
) -> CoreOwnedItem {
CoreOwnedItem {
rating: 0,
source_rating,
core_content_kind: Some(core_kind.to_string()),
..owned(owned_id, card)
}
}
fn apply_test_server(items: &[CoreOwnedItem]) -> (Server, Arc<Fifa17IdentityResolver>) {
let core = Arc::new(FakeSbcCore {
owned: Mutex::new(items.to_vec()),
..FakeSbcCore::default()
});
let store =
openfut_identity::JsonIdentityStore::open(temp_store_path("apply-identity")).unwrap();
let resolver = Arc::new(Fifa17IdentityResolver::new(
apply_catalog(),
Arc::new(store),
));
let server = Server::new(
core,
Arc::new(Fifa17Entities::default()),
resolver.clone(),
Arc::new(PassClient::new("http://127.0.0.1:9")),
1,
);
(server, resolver)
}
/// The wire id the client holds for this instance, minted through the
/// PRODUCTION resolver so the handler reverses exactly what production would.
fn wire_of(resolver: &Fifa17IdentityResolver, it: &CoreOwnedItem) -> i64 {
let minted = match ItemIdentityResolver::kind_of(resolver, it) {
ContentKind::Player => resolver.resolve(it).map(|i| i.item_id),
_ => resolver.resolve_staff(it).map(|i| i.item_id),
};
i64::from(minted.expect("a catalogued fixture must resolve to a wire id"))
}
fn apply_body(wire: i64) -> Vec<u8> {
format!("{{\"apply\":[{{\"id\":{wire}}}]}}").into_bytes()
}
/// THE discriminating case. `5001008` is `[bronze 8, silver 10, gold 8]`, the
/// manager target's `value` is 88 (GOLD) and the CARD is silver, so:
/// * 8 = the target's tier, which is the invariant.
/// * 10 = the card's own tier — the classic backwards read.
///
/// It also proves the `target_kind` Core receives is CORE's token (`manager`),
/// not the catalog's (`staff`), which is what Core's `require_kind` compares.
#[test]
fn manager_contract_grants_the_target_managers_tier() {
let source = owned("oc-contract-mgr", "contract_manager");
let target = staff_owned("oc-manager", "card_manager", Some(88), "manager");
let (server, resolver) = apply_test_server(&[source, target.clone()]);
let wire = wire_of(&resolver, &target);
let econ = FakeEconomy::ok(0, 0);
let resp = server.handle_consumable_apply(5_001_008, &apply_body(wire), &econ);
assert_eq!(resp.status, 200, "a gold manager is a servable 202 target");
assert_eq!(resp.body, br#"{"itemData":[]}"#);
let applied = econ.applies.lock().clone();
assert_eq!(
applied,
vec![RecordedApply {
target_owned_card_id: "oc-manager".to_string(),
target_kind: "manager".to_string(),
amount: 8,
}],
"gold TARGET column (8), not the silver CARD column (10)"
);
}
/// The regression that made 202 unservable: Core keeps a non-player's
/// `overall` at 0, and 0 scores BRONZE. `5001008`'s bronze column is 8 and its
/// silver column is 10, so a silver (`value` 66) manager separates the two
/// reads — 10 proves `source_rating` was read, 8 would prove `overall` was.
#[test]
fn a_manager_targets_tier_comes_from_source_rating_not_cores_zero_overall() {
let source = owned("oc-contract-mgr", "contract_manager");
let target = staff_owned("oc-manager", "card_manager", Some(66), "manager");
let (server, resolver) = apply_test_server(&[source, target.clone()]);
let wire = wire_of(&resolver, &target);
let econ = FakeEconomy::ok(0, 0);
let resp = server.handle_consumable_apply(5_001_008, &apply_body(wire), &econ);
assert_eq!(resp.status, 200);
assert_eq!(target.rating, 0, "Core's own `overall` for a non-player");
assert_eq!(
econ.applies.lock()[0].amount,
10,
"silver TARGET column (10); reading `overall` 0 would have paid bronze (8)"
);
}
/// A COACH is not a manager. All five staff families share
/// `ContentKind::Staff`, so only `cardsubtypeid` 4 may be granted manager
/// contracts — and the refusal must mutate nothing.
#[test]
fn manager_contract_refuses_a_coach_target() {
let source = owned("oc-contract-mgr", "contract_manager");
let target = staff_owned("oc-coach", "card_coach", Some(66), "staff");
let (server, resolver) = apply_test_server(&[source, target.clone()]);
let wire = wire_of(&resolver, &target);
let econ = FakeEconomy::ok(0, 0);
let resp = server.handle_consumable_apply(5_001_008, &apply_body(wire), &econ);
assert_eq!(resp.status, 409);
assert_eq!(resp.body, br#"{"error":"contract_target_not_a_manager"}"#);
assert!(econ.applies.lock().is_empty(), "nothing was mutated");
}
/// A PLAYER is not a manager either: 202 on a footballer has no proven effect,
/// and a player's catalog subtype is 0, so it fails the same gate.
#[test]
fn manager_contract_refuses_a_player_target() {
let source = owned("oc-contract-mgr", "contract_manager");
let target = owned("oc-player", "card_player");
let (server, resolver) = apply_test_server(&[source, target.clone()]);
let wire = wire_of(&resolver, &target);
let econ = FakeEconomy::ok(0, 0);
let resp = server.handle_consumable_apply(5_001_008, &apply_body(wire), &econ);
assert_eq!(resp.status, 409);
assert_eq!(resp.body, br#"{"error":"contract_target_not_a_manager"}"#);
assert!(econ.applies.lock().is_empty(), "nothing was mutated");
}
/// The mirror gate, unchanged: a PLAYER contract on a manager is refused by
/// kind, so implementing 202 did not loosen 201.
#[test]
fn player_contract_refuses_a_manager_target() {
let source = owned("oc-contract-plr", "contract_player");
let target = staff_owned("oc-manager", "card_manager", Some(88), "manager");
let (server, resolver) = apply_test_server(&[source, target.clone()]);
let wire = wire_of(&resolver, &target);
let econ = FakeEconomy::ok(0, 0);
let resp = server.handle_consumable_apply(5_001_002, &apply_body(wire), &econ);
assert_eq!(resp.status, 409);
assert_eq!(resp.body, br#"{"error":"contract_target_not_a_player"}"#);
assert!(econ.applies.lock().is_empty(), "nothing was mutated");
}
/// FAIL CLOSED. A manager Core carries no `source_rating` for has no honest
/// tier, and every possible default is a silent mis-credit: bronze under-pays
/// a gold manager, gold over-pays a bronze one. Refuse, mutate nothing.
#[test]
fn an_unrated_manager_target_is_refused_rather_than_defaulted() {
let source = owned("oc-contract-mgr", "contract_manager");
let target = staff_owned("oc-manager", "card_manager", None, "manager");
let (server, resolver) = apply_test_server(&[source, target.clone()]);
let wire = wire_of(&resolver, &target);
let econ = FakeEconomy::ok(0, 0);
let resp = server.handle_consumable_apply(5_001_008, &apply_body(wire), &econ);
assert_eq!(resp.status, 409);
assert_eq!(resp.body, br#"{"error":"manager_tier_unknown"}"#);
assert!(
econ.applies.lock().is_empty(),
"an unknown tier must never reach Core"
);
}
#[test]
fn marketdata_container_types_are_load_bearing() {
// /pricelimits MUST be a bare ARRAY (object-where-array froze a live client).
+2
View File
@@ -1040,6 +1040,8 @@ mod tests {
club: String::new(),
attributes: [80, 80, 80, 80, 80, 80],
contract_matches: None,
source_rating: None,
core_content_kind: None,
}
}