NEO-144: validate contract templates in content CI
Add roster, freeze, cross-ref, and band-1 economy cap gates for E7M4 prototype templates.pull/184/head
parent
58d2c32152
commit
7ecc8746f5
|
|
@ -18,6 +18,8 @@ Validates:
|
||||||
optional completionRewardBundle via quest-reward-bundle.schema.json (NEO-124);
|
optional completionRewardBundle via quest-reward-bundle.schema.json (NEO-124);
|
||||||
optional factionGateRules + reputationGrants (NEO-133)
|
optional factionGateRules + reputationGrants (NEO-133)
|
||||||
- faction catalogs: content/factions/*_factions.json rows vs content/schemas/faction-def.schema.json (NEO-133)
|
- faction catalogs: content/factions/*_factions.json rows vs content/schemas/faction-def.schema.json (NEO-133)
|
||||||
|
- contract template catalogs: content/contracts/*_contract_templates.json rows vs
|
||||||
|
content/schemas/contract-template.schema.json (NEO-144); completionRewardBundle via quest-reward-bundle.schema.json
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
@ -51,6 +53,8 @@ QUEST_SKILL_XP_GRANT_SCHEMA = REPO_ROOT / "content/schemas/quest-skill-xp-grant.
|
||||||
FACTION_DEF_SCHEMA = REPO_ROOT / "content/schemas/faction-def.schema.json"
|
FACTION_DEF_SCHEMA = REPO_ROOT / "content/schemas/faction-def.schema.json"
|
||||||
FACTION_GATE_RULE_SCHEMA = REPO_ROOT / "content/schemas/faction-gate-rule.schema.json"
|
FACTION_GATE_RULE_SCHEMA = REPO_ROOT / "content/schemas/faction-gate-rule.schema.json"
|
||||||
REPUTATION_GRANT_ROW_SCHEMA = REPO_ROOT / "content/schemas/reputation-grant-row.schema.json"
|
REPUTATION_GRANT_ROW_SCHEMA = REPO_ROOT / "content/schemas/reputation-grant-row.schema.json"
|
||||||
|
CONTRACT_TEMPLATE_SCHEMA = REPO_ROOT / "content/schemas/contract-template.schema.json"
|
||||||
|
CONTRACT_SEED_SCHEMA = REPO_ROOT / "content/schemas/contract-seed.schema.json"
|
||||||
SKILLS_DIR = REPO_ROOT / "content/skills"
|
SKILLS_DIR = REPO_ROOT / "content/skills"
|
||||||
MASTERY_DIR = REPO_ROOT / "content/mastery"
|
MASTERY_DIR = REPO_ROOT / "content/mastery"
|
||||||
ITEMS_DIR = REPO_ROOT / "content/items"
|
ITEMS_DIR = REPO_ROOT / "content/items"
|
||||||
|
|
@ -62,6 +66,7 @@ REWARD_TABLES_DIR = REPO_ROOT / "content/reward-tables"
|
||||||
ENCOUNTERS_DIR = REPO_ROOT / "content/encounters"
|
ENCOUNTERS_DIR = REPO_ROOT / "content/encounters"
|
||||||
QUESTS_DIR = REPO_ROOT / "content/quests"
|
QUESTS_DIR = REPO_ROOT / "content/quests"
|
||||||
FACTIONS_DIR = REPO_ROOT / "content/factions"
|
FACTIONS_DIR = REPO_ROOT / "content/factions"
|
||||||
|
CONTRACTS_DIR = REPO_ROOT / "content/contracts"
|
||||||
|
|
||||||
# Slice 1 prototype lock (NEO-33): exact ids + category coverage after schema passes.
|
# Slice 1 prototype lock (NEO-33): exact ids + category coverage after schema passes.
|
||||||
PROTOTYPE_SLICE1_SKILL_IDS = frozenset({"salvage", "refine", "intrusion"})
|
PROTOTYPE_SLICE1_SKILL_IDS = frozenset({"salvage", "refine", "intrusion"})
|
||||||
|
|
@ -270,6 +275,33 @@ PROTOTYPE_E7M3_COMPLETION_BUNDLES: dict[str, dict] = {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Epic 7 Slice 4 prototype lock (NEO-144): one contract template + band-1 economy caps.
|
||||||
|
# Keep in sync with E7.M4 freeze table and future NEO-145 server loader.
|
||||||
|
PROTOTYPE_E7M4_TEMPLATE_IDS = frozenset({"prototype_contract_clear_combat_pocket"})
|
||||||
|
PROTOTYPE_E7M4_TEMPLATE_FREEZE: dict[str, dict] = {
|
||||||
|
"prototype_contract_clear_combat_pocket": {
|
||||||
|
"displayName": "Clear Combat Pocket (Repeat)",
|
||||||
|
"zoneDifficultyBand": 1,
|
||||||
|
"objectiveKind": "encounter_clear",
|
||||||
|
"encounterTemplateId": "prototype_combat_pocket",
|
||||||
|
"minFactionStanding": {
|
||||||
|
"factionId": "prototype_faction_grid_operators",
|
||||||
|
"minStanding": 0,
|
||||||
|
},
|
||||||
|
"completionRewardBundle": {
|
||||||
|
"itemGrants": [{"itemId": "scrap_metal_bulk", "quantity": 5}],
|
||||||
|
"skillXpGrants": [{"skillId": "salvage", "amount": 15}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
PROTOTYPE_E7M4_BAND_CAPS: dict[int, dict[str, int]] = {
|
||||||
|
1: {
|
||||||
|
"maxItemQuantity": 10,
|
||||||
|
"maxSkillXpAmount": 25,
|
||||||
|
"maxReputationAmount": 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _normalize_completion_bundle(bundle: dict) -> dict:
|
def _normalize_completion_bundle(bundle: dict) -> dict:
|
||||||
"""Normalize grant rows for stable freeze-table comparison."""
|
"""Normalize grant rows for stable freeze-table comparison."""
|
||||||
|
|
@ -1011,6 +1043,27 @@ def _quest_def_validator() -> Draft202012Validator:
|
||||||
return Draft202012Validator(def_schema, registry=registry)
|
return Draft202012Validator(def_schema, registry=registry)
|
||||||
|
|
||||||
|
|
||||||
|
def _contract_template_validator() -> Draft202012Validator:
|
||||||
|
"""Build a validator that resolves contract-template $ref to bundle and gate schemas."""
|
||||||
|
template_schema = json.loads(CONTRACT_TEMPLATE_SCHEMA.read_text(encoding="utf-8"))
|
||||||
|
bundle_schema = json.loads(QUEST_REWARD_BUNDLE_SCHEMA.read_text(encoding="utf-8"))
|
||||||
|
skill_xp_grant_schema = json.loads(QUEST_SKILL_XP_GRANT_SCHEMA.read_text(encoding="utf-8"))
|
||||||
|
grant_row_schema = json.loads(REWARD_GRANT_ROW_SCHEMA.read_text(encoding="utf-8"))
|
||||||
|
faction_gate_rule_schema = json.loads(FACTION_GATE_RULE_SCHEMA.read_text(encoding="utf-8"))
|
||||||
|
reputation_grant_row_schema = json.loads(REPUTATION_GRANT_ROW_SCHEMA.read_text(encoding="utf-8"))
|
||||||
|
registry = Registry().with_resources(
|
||||||
|
[
|
||||||
|
(template_schema["$id"], Resource.from_contents(template_schema)),
|
||||||
|
(bundle_schema["$id"], Resource.from_contents(bundle_schema)),
|
||||||
|
(skill_xp_grant_schema["$id"], Resource.from_contents(skill_xp_grant_schema)),
|
||||||
|
(grant_row_schema["$id"], Resource.from_contents(grant_row_schema)),
|
||||||
|
(faction_gate_rule_schema["$id"], Resource.from_contents(faction_gate_rule_schema)),
|
||||||
|
(reputation_grant_row_schema["$id"], Resource.from_contents(reputation_grant_row_schema)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return Draft202012Validator(template_schema, registry=registry)
|
||||||
|
|
||||||
|
|
||||||
def _validate_faction_catalogs(
|
def _validate_faction_catalogs(
|
||||||
*,
|
*,
|
||||||
faction_files: list[Path],
|
faction_files: list[Path],
|
||||||
|
|
@ -1143,6 +1196,55 @@ def _validate_quest_catalogs(
|
||||||
return errors, seen_ids, id_to_row
|
return errors, seen_ids, id_to_row
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_contract_catalogs(
|
||||||
|
*,
|
||||||
|
contract_files: list[Path],
|
||||||
|
contract_validator: Draft202012Validator,
|
||||||
|
) -> tuple[int, dict[str, str], dict[str, dict]]:
|
||||||
|
"""Validate contract template JSON files. Returns (error_count, seen_ids, id_to_row)."""
|
||||||
|
errors = 0
|
||||||
|
seen_ids: dict[str, str] = {}
|
||||||
|
id_to_row: dict[str, dict] = {}
|
||||||
|
|
||||||
|
for path in contract_files:
|
||||||
|
rel = str(path.relative_to(REPO_ROOT))
|
||||||
|
data = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
schema_version = data.get("schemaVersion")
|
||||||
|
if schema_version != 1:
|
||||||
|
print(f"error: {rel}: expected schemaVersion 1, got {schema_version!r}", file=sys.stderr)
|
||||||
|
errors += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
contract_templates = data.get("contractTemplates")
|
||||||
|
if not isinstance(contract_templates, list):
|
||||||
|
print(f"error: {rel}: expected top-level 'contractTemplates' array", file=sys.stderr)
|
||||||
|
errors += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
for i, row in enumerate(contract_templates):
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
print(f"error: {rel}: contractTemplates[{i}] must be an object", file=sys.stderr)
|
||||||
|
errors += 1
|
||||||
|
continue
|
||||||
|
row_schema_errors = 0
|
||||||
|
for err in sorted(contract_validator.iter_errors(row), key=lambda e: e.path):
|
||||||
|
loc = ".".join(str(p) for p in err.path) or "(root)"
|
||||||
|
print(f"error: {rel} contractTemplates[{i}] {loc}: {err.message}", file=sys.stderr)
|
||||||
|
row_schema_errors += 1
|
||||||
|
errors += 1
|
||||||
|
tid = row.get("id")
|
||||||
|
if isinstance(tid, str) and row_schema_errors == 0:
|
||||||
|
prev = seen_ids.get(tid)
|
||||||
|
if prev:
|
||||||
|
print(f"error: duplicate contract template id {tid!r} in {prev} and {rel}", file=sys.stderr)
|
||||||
|
errors += 1
|
||||||
|
else:
|
||||||
|
seen_ids[tid] = rel
|
||||||
|
id_to_row[tid] = row
|
||||||
|
|
||||||
|
return errors, seen_ids, id_to_row
|
||||||
|
|
||||||
|
|
||||||
def _prototype_e7m3_quest_roster_gate(seen_ids: dict[str, str]) -> str | None:
|
def _prototype_e7m3_quest_roster_gate(seen_ids: dict[str, str]) -> str | None:
|
||||||
"""Return a human-readable error if E7M3 five-quest roster fails, else None."""
|
"""Return a human-readable error if E7M3 five-quest roster fails, else None."""
|
||||||
ids = frozenset(seen_ids.keys())
|
ids = frozenset(seen_ids.keys())
|
||||||
|
|
@ -1510,6 +1612,179 @@ def _prototype_e7m3_grid_contract_shape_gate(id_to_row: dict[str, dict]) -> str
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _prototype_e7m4_template_roster_gate(seen_ids: dict[str, str]) -> str | None:
|
||||||
|
"""Return a human-readable error if E7M4 template roster fails, else None."""
|
||||||
|
ids = frozenset(seen_ids.keys())
|
||||||
|
if ids != PROTOTYPE_E7M4_TEMPLATE_IDS:
|
||||||
|
return (
|
||||||
|
"error: prototype E7M4 expects exactly contract template ids "
|
||||||
|
f"{sorted(PROTOTYPE_E7M4_TEMPLATE_IDS)!r}, got {sorted(ids)!r}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _prototype_e7m4_template_freeze_gate(id_to_row: dict[str, dict]) -> str | None:
|
||||||
|
"""Return a human-readable error if template rows diverge from E7M4 freeze table, else None."""
|
||||||
|
for tid, expected in PROTOTYPE_E7M4_TEMPLATE_FREEZE.items():
|
||||||
|
row = id_to_row.get(tid)
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
return f"error: missing contract template {tid!r}"
|
||||||
|
for field in (
|
||||||
|
"displayName",
|
||||||
|
"zoneDifficultyBand",
|
||||||
|
"objectiveKind",
|
||||||
|
"encounterTemplateId",
|
||||||
|
):
|
||||||
|
if row.get(field) != expected[field]:
|
||||||
|
return (
|
||||||
|
f"error: contract template {tid!r} {field} must be "
|
||||||
|
f"{expected[field]!r}, got {row.get(field)!r}"
|
||||||
|
)
|
||||||
|
min_standing = row.get("minFactionStanding")
|
||||||
|
if min_standing != expected["minFactionStanding"]:
|
||||||
|
return (
|
||||||
|
f"error: contract template {tid!r} minFactionStanding must be "
|
||||||
|
f"{expected['minFactionStanding']!r}, got {min_standing!r}"
|
||||||
|
)
|
||||||
|
bundle = row.get("completionRewardBundle")
|
||||||
|
if not isinstance(bundle, dict):
|
||||||
|
return f"error: contract template {tid!r} must include completionRewardBundle object"
|
||||||
|
actual_bundle = _normalize_completion_bundle(bundle)
|
||||||
|
expected_bundle = _normalize_completion_bundle(expected["completionRewardBundle"])
|
||||||
|
if actual_bundle != expected_bundle:
|
||||||
|
return (
|
||||||
|
f"error: contract template {tid!r} completionRewardBundle must match E7M4 freeze table "
|
||||||
|
f"(expected {expected_bundle!r}, got {actual_bundle!r})"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _prototype_e7m4_cross_ref_gate(
|
||||||
|
id_to_row: dict[str, dict],
|
||||||
|
encounter_seen_ids: dict[str, str],
|
||||||
|
faction_seen_ids: dict[str, str],
|
||||||
|
skill_id_to_allowed_kinds: dict[str, list[str]],
|
||||||
|
) -> str | None:
|
||||||
|
"""Return a human-readable error if contract template refs fail cross-checks, else None."""
|
||||||
|
for tid in sorted(PROTOTYPE_E7M4_TEMPLATE_IDS):
|
||||||
|
row = id_to_row.get(tid)
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
continue
|
||||||
|
encounter_id = row.get("encounterTemplateId")
|
||||||
|
if isinstance(encounter_id, str) and encounter_id not in encounter_seen_ids:
|
||||||
|
return (
|
||||||
|
f"error: contract template {tid!r} encounterTemplateId {encounter_id!r} "
|
||||||
|
"is not in the loaded encounter catalog"
|
||||||
|
)
|
||||||
|
min_standing = row.get("minFactionStanding")
|
||||||
|
if isinstance(min_standing, dict):
|
||||||
|
faction_id = min_standing.get("factionId")
|
||||||
|
if isinstance(faction_id, str) and faction_id not in faction_seen_ids:
|
||||||
|
return (
|
||||||
|
f"error: contract template {tid!r} minFactionStanding.factionId "
|
||||||
|
f"{faction_id!r} is not in the frozen prototype faction catalog"
|
||||||
|
)
|
||||||
|
bundle = row.get("completionRewardBundle")
|
||||||
|
if not isinstance(bundle, dict):
|
||||||
|
continue
|
||||||
|
item_grants = bundle.get("itemGrants")
|
||||||
|
if isinstance(item_grants, list):
|
||||||
|
for gi, grant in enumerate(item_grants):
|
||||||
|
if not isinstance(grant, dict):
|
||||||
|
continue
|
||||||
|
item_id = grant.get("itemId")
|
||||||
|
if isinstance(item_id, str) and item_id not in PROTOTYPE_SLICE1_ITEM_IDS:
|
||||||
|
return (
|
||||||
|
f"error: contract template {tid!r} completionRewardBundle.itemGrants[{gi}]: "
|
||||||
|
f"itemId {item_id!r} is not in the frozen prototype item catalog"
|
||||||
|
)
|
||||||
|
skill_xp_grants = bundle.get("skillXpGrants")
|
||||||
|
if isinstance(skill_xp_grants, list):
|
||||||
|
for gi, grant in enumerate(skill_xp_grants):
|
||||||
|
if not isinstance(grant, dict):
|
||||||
|
continue
|
||||||
|
skill_id = grant.get("skillId")
|
||||||
|
if isinstance(skill_id, str) and skill_id not in PROTOTYPE_SLICE1_SKILL_IDS:
|
||||||
|
return (
|
||||||
|
f"error: contract template {tid!r} completionRewardBundle.skillXpGrants[{gi}]: "
|
||||||
|
f"skillId {skill_id!r} is not in the frozen prototype skill catalog"
|
||||||
|
)
|
||||||
|
allowed_kinds = skill_id_to_allowed_kinds.get(skill_id, [])
|
||||||
|
if PROTOTYPE_E7M2_MISSION_REWARD_SOURCE_KIND not in allowed_kinds:
|
||||||
|
return (
|
||||||
|
f"error: contract template {tid!r} completionRewardBundle.skillXpGrants[{gi}]: "
|
||||||
|
f"skillId {skill_id!r} must allow sourceKind "
|
||||||
|
f"{PROTOTYPE_E7M2_MISSION_REWARD_SOURCE_KIND!r} in allowedXpSourceKinds "
|
||||||
|
f"(got {allowed_kinds!r})"
|
||||||
|
)
|
||||||
|
reputation_grants = bundle.get("reputationGrants")
|
||||||
|
if isinstance(reputation_grants, list):
|
||||||
|
for gi, grant in enumerate(reputation_grants):
|
||||||
|
if not isinstance(grant, dict):
|
||||||
|
continue
|
||||||
|
faction_id = grant.get("factionId")
|
||||||
|
if isinstance(faction_id, str) and faction_id not in faction_seen_ids:
|
||||||
|
return (
|
||||||
|
f"error: contract template {tid!r} completionRewardBundle.reputationGrants[{gi}]: "
|
||||||
|
f"factionId {faction_id!r} is not in the frozen prototype faction catalog"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _prototype_e7m4_band_cap_gate(id_to_row: dict[str, dict]) -> str | None:
|
||||||
|
"""Return a human-readable error if a template bundle exceeds band caps, else None."""
|
||||||
|
for tid in sorted(PROTOTYPE_E7M4_TEMPLATE_IDS):
|
||||||
|
row = id_to_row.get(tid)
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
continue
|
||||||
|
band = row.get("zoneDifficultyBand")
|
||||||
|
if not isinstance(band, int):
|
||||||
|
continue
|
||||||
|
caps = PROTOTYPE_E7M4_BAND_CAPS.get(band)
|
||||||
|
if caps is None:
|
||||||
|
return (
|
||||||
|
f"error: contract template {tid!r} zoneDifficultyBand {band} has no "
|
||||||
|
"prototype economy cap policy"
|
||||||
|
)
|
||||||
|
bundle = row.get("completionRewardBundle")
|
||||||
|
if not isinstance(bundle, dict):
|
||||||
|
continue
|
||||||
|
item_grants = bundle.get("itemGrants")
|
||||||
|
if isinstance(item_grants, list):
|
||||||
|
for gi, grant in enumerate(item_grants):
|
||||||
|
if not isinstance(grant, dict):
|
||||||
|
continue
|
||||||
|
quantity = grant.get("quantity")
|
||||||
|
if isinstance(quantity, int) and quantity > caps["maxItemQuantity"]:
|
||||||
|
return (
|
||||||
|
f"error: contract template {tid!r} completionRewardBundle.itemGrants[{gi}] "
|
||||||
|
f"quantity {quantity} exceeds band {band} cap {caps['maxItemQuantity']}"
|
||||||
|
)
|
||||||
|
skill_xp_grants = bundle.get("skillXpGrants")
|
||||||
|
if isinstance(skill_xp_grants, list):
|
||||||
|
for gi, grant in enumerate(skill_xp_grants):
|
||||||
|
if not isinstance(grant, dict):
|
||||||
|
continue
|
||||||
|
amount = grant.get("amount")
|
||||||
|
if isinstance(amount, int) and amount > caps["maxSkillXpAmount"]:
|
||||||
|
return (
|
||||||
|
f"error: contract template {tid!r} completionRewardBundle.skillXpGrants[{gi}] "
|
||||||
|
f"amount {amount} exceeds band {band} cap {caps['maxSkillXpAmount']}"
|
||||||
|
)
|
||||||
|
reputation_grants = bundle.get("reputationGrants")
|
||||||
|
if isinstance(reputation_grants, list):
|
||||||
|
for gi, grant in enumerate(reputation_grants):
|
||||||
|
if not isinstance(grant, dict):
|
||||||
|
continue
|
||||||
|
amount = grant.get("amount")
|
||||||
|
if isinstance(amount, int) and abs(amount) > caps["maxReputationAmount"]:
|
||||||
|
return (
|
||||||
|
f"error: contract template {tid!r} completionRewardBundle.reputationGrants[{gi}] "
|
||||||
|
f"amount {amount} exceeds band {band} rep cap {caps['maxReputationAmount']}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _prototype_slice4_gate(track_skill_ids: list[str]) -> str | None:
|
def _prototype_slice4_gate(track_skill_ids: list[str]) -> str | None:
|
||||||
"""Return a human-readable error if Slice 4 contract fails, else None."""
|
"""Return a human-readable error if Slice 4 contract fails, else None."""
|
||||||
if len(track_skill_ids) != 1:
|
if len(track_skill_ids) != 1:
|
||||||
|
|
@ -1891,6 +2166,7 @@ def main() -> int:
|
||||||
encounter_schema = json.loads(ENCOUNTER_DEF_SCHEMA.read_text(encoding="utf-8"))
|
encounter_schema = json.loads(ENCOUNTER_DEF_SCHEMA.read_text(encoding="utf-8"))
|
||||||
encounter_validator = Draft202012Validator(encounter_schema)
|
encounter_validator = Draft202012Validator(encounter_schema)
|
||||||
quest_validator = _quest_def_validator()
|
quest_validator = _quest_def_validator()
|
||||||
|
contract_validator = _contract_template_validator()
|
||||||
faction_schema = json.loads(FACTION_DEF_SCHEMA.read_text(encoding="utf-8"))
|
faction_schema = json.loads(FACTION_DEF_SCHEMA.read_text(encoding="utf-8"))
|
||||||
faction_validator = Draft202012Validator(faction_schema)
|
faction_validator = Draft202012Validator(faction_schema)
|
||||||
|
|
||||||
|
|
@ -2003,6 +2279,15 @@ def main() -> int:
|
||||||
print(f"error: no *_factions.json files under {FACTIONS_DIR}", file=sys.stderr)
|
print(f"error: no *_factions.json files under {FACTIONS_DIR}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
if not CONTRACTS_DIR.is_dir():
|
||||||
|
print(f"error: missing directory {CONTRACTS_DIR}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
contract_files = sorted(CONTRACTS_DIR.glob("*_contract_templates.json"))
|
||||||
|
if not contract_files:
|
||||||
|
print(f"error: no *_contract_templates.json files under {CONTRACTS_DIR}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
seen_ids: dict[str, str] = {}
|
seen_ids: dict[str, str] = {}
|
||||||
id_to_category: dict[str, str] = {}
|
id_to_category: dict[str, str] = {}
|
||||||
skill_id_to_allowed_kinds: dict[str, list[str]] = {}
|
skill_id_to_allowed_kinds: dict[str, list[str]] = {}
|
||||||
|
|
@ -2297,6 +2582,39 @@ def main() -> int:
|
||||||
print(e7m3_faction_band_err, file=sys.stderr)
|
print(e7m3_faction_band_err, file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
contract_errors, contract_seen_ids, contract_id_to_row = _validate_contract_catalogs(
|
||||||
|
contract_files=contract_files,
|
||||||
|
contract_validator=contract_validator,
|
||||||
|
)
|
||||||
|
if contract_errors:
|
||||||
|
print(f"content validation failed with {contract_errors} error(s)", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
e7m4_template_roster_err = _prototype_e7m4_template_roster_gate(contract_seen_ids)
|
||||||
|
if e7m4_template_roster_err:
|
||||||
|
print(e7m4_template_roster_err, file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
e7m4_template_freeze_err = _prototype_e7m4_template_freeze_gate(contract_id_to_row)
|
||||||
|
if e7m4_template_freeze_err:
|
||||||
|
print(e7m4_template_freeze_err, file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
e7m4_cross_ref_err = _prototype_e7m4_cross_ref_gate(
|
||||||
|
contract_id_to_row,
|
||||||
|
encounter_seen_ids,
|
||||||
|
faction_seen_ids,
|
||||||
|
skill_id_to_allowed_kinds,
|
||||||
|
)
|
||||||
|
if e7m4_cross_ref_err:
|
||||||
|
print(e7m4_cross_ref_err, file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
e7m4_band_cap_err = _prototype_e7m4_band_cap_gate(contract_id_to_row)
|
||||||
|
if e7m4_band_cap_err:
|
||||||
|
print(e7m4_band_cap_err, file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
quest_errors, quest_seen_ids, quest_id_to_row = _validate_quest_catalogs(
|
quest_errors, quest_seen_ids, quest_id_to_row = _validate_quest_catalogs(
|
||||||
quest_files=quest_files,
|
quest_files=quest_files,
|
||||||
quest_validator=quest_validator,
|
quest_validator=quest_validator,
|
||||||
|
|
@ -2375,6 +2693,7 @@ def main() -> int:
|
||||||
f"{len(reward_table_files)} reward table catalog file(s), "
|
f"{len(reward_table_files)} reward table catalog file(s), "
|
||||||
f"{len(encounter_files)} encounter catalog file(s), "
|
f"{len(encounter_files)} encounter catalog file(s), "
|
||||||
f"{len(faction_files)} faction catalog file(s), "
|
f"{len(faction_files)} faction catalog file(s), "
|
||||||
|
f"{len(contract_files)} contract template catalog file(s), "
|
||||||
f"{len(quest_files)} quest catalog file(s), "
|
f"{len(quest_files)} quest catalog file(s), "
|
||||||
f"{len(seen_ids)} unique skill id(s), "
|
f"{len(seen_ids)} unique skill id(s), "
|
||||||
f"{len(item_seen_ids)} unique item id(s), "
|
f"{len(item_seen_ids)} unique item id(s), "
|
||||||
|
|
@ -2385,6 +2704,7 @@ def main() -> int:
|
||||||
f"{len(reward_table_seen_ids)} unique reward table id(s), "
|
f"{len(reward_table_seen_ids)} unique reward table id(s), "
|
||||||
f"{len(encounter_seen_ids)} unique encounter id(s), "
|
f"{len(encounter_seen_ids)} unique encounter id(s), "
|
||||||
f"{len(faction_seen_ids)} unique faction id(s), "
|
f"{len(faction_seen_ids)} unique faction id(s), "
|
||||||
|
f"{len(contract_seen_ids)} unique contract template id(s), "
|
||||||
f"{len(quest_seen_ids)} unique quest id(s), "
|
f"{len(quest_seen_ids)} unique quest id(s), "
|
||||||
f"{len(track_skill_ids)} mastery track(s)"
|
f"{len(track_skill_ids)} mastery track(s)"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue