596 lines
22 KiB
GDScript
596 lines
22 KiB
GDScript
class_name FG3ContentRegistry
|
|
extends RefCounted
|
|
|
|
const CATALOG_PATH := "res://fg3/catalog.json"
|
|
const SCHEMA_VERSION := 1
|
|
const CONTRACT_REVISION := "fg3-complete-rg02-r1"
|
|
const SOURCE_AUTHORITY := {
|
|
"region_catalog_blob": "2ca533feb82bdedf6f141c6381319c3a56759ba3",
|
|
"enemy_catalog_blob": "cdf05128cfd1832835712e37c3eb200595983613",
|
|
"equipment_catalog_blob": "3fd34ef951056b02749137f7bff2e66fb94b8375",
|
|
"artifact_catalog_blob": "009265f793e646a0ebb45d4018eb6c2d2c6e51e3",
|
|
"economy_blob": "0cb57e7b44d983f61c27e8b60da43d4728950e33",
|
|
"systems_blob": "7145086919197bac5a01917fca36e4f96619e5ea",
|
|
"p10_3_catalog_blob": "b49c01506742dd35c884066ae312eab51396d6da",
|
|
}
|
|
const EXPECTED_IDS := {
|
|
"equipment": [
|
|
"EQ-065", "EQ-066", "EQ-067", "EQ-068", "EQ-069", "EQ-070",
|
|
"EQ-071", "EQ-072", "EQ-073", "EQ-074", "EQ-075", "EQ-076",
|
|
"EQ-077", "EQ-078", "EQ-079", "EQ-080",
|
|
],
|
|
"artifacts": ["AR-009", "AR-010", "AR-013", "AR-014"],
|
|
"bosses": ["BO-R2-01", "BO-R2-02"],
|
|
"routes": [
|
|
"RO-R2-M08-Q0", "RO-R2-M08-Q1", "RO-R2-M08-Q2",
|
|
"RO-R2-M09-Q0", "RO-R2-M09-Q1", "RO-R2-M09-Q2",
|
|
"RO-R2-M10-Q0", "RO-R2-M10-Q1", "RO-R2-M10-Q2",
|
|
"RO-R2-M11-Q0", "RO-R2-M11-Q1", "RO-R2-M11-Q2",
|
|
"RO-R2-M12-Q0", "RO-R2-M12-Q1", "RO-R2-M12-Q2",
|
|
],
|
|
}
|
|
const EXPECTED_INHERITED := {
|
|
"equipment_ids": [
|
|
"EQ-041", "EQ-042", "EQ-043", "EQ-044", "EQ-045", "EQ-046",
|
|
"EQ-047", "EQ-048", "EQ-049", "EQ-050", "EQ-051", "EQ-052",
|
|
"EQ-053", "EQ-054", "EQ-055", "EQ-056", "EQ-057", "EQ-058",
|
|
"EQ-059", "EQ-060", "EQ-061", "EQ-062", "EQ-063", "EQ-064",
|
|
],
|
|
"enemy_ids": [
|
|
"EN-R2-01", "EN-R2-02", "EN-R2-03", "EN-R2-04", "EN-R2-05",
|
|
"EN-R2-06", "EN-R2-07", "EN-R2-08", "EN-R2-09", "EN-R2-10",
|
|
"EN-R2-11", "EN-R2-12",
|
|
],
|
|
"artifact_ids": ["AR-011", "AR-012", "AR-015"],
|
|
}
|
|
const REQUIRED_FIELDS := {
|
|
"equipment": ["fit", "class", "effect", "tradeoff"],
|
|
"artifacts": ["rule", "tradeoff", "conflict_id"],
|
|
"bosses": ["rule", "responses", "failure_cause", "phase_change", "executable"],
|
|
"routes": [
|
|
"market_index", "round_index", "risk", "enemy_ids", "wave_order",
|
|
"reward_coin", "recovery_coin", "pressure", "failure_cause",
|
|
],
|
|
}
|
|
const FILLER_TERMS := ["todo", "tbd", "placeholder", "same as", "generic"]
|
|
|
|
|
|
static func snapshot(path: String = CATALOG_PATH) -> Dictionary:
|
|
var text := FileAccess.get_file_as_string(path)
|
|
if text.is_empty():
|
|
return {"load_error": "catalog_unreadable:%s" % path}
|
|
var parsed = JSON.parse_string(text)
|
|
if not parsed is Dictionary:
|
|
return {"load_error": "catalog_not_dictionary"}
|
|
var content: Dictionary = parsed.duplicate(true)
|
|
_enrich(content)
|
|
content["catalog_hash"] = _hash(content)
|
|
return content
|
|
|
|
|
|
static func validate(content: Dictionary) -> Dictionary:
|
|
var errors: Array[String] = []
|
|
if content.has("load_error"):
|
|
return {"ok": false, "errors": [String(content.load_error)]}
|
|
if int(content.get("schema", -1)) != SCHEMA_VERSION:
|
|
errors.append("schema")
|
|
if String(content.get("contract_revision", "")) != CONTRACT_REVISION:
|
|
errors.append("contract_revision")
|
|
_validate_source(content.get("source", {}), errors)
|
|
_validate_inherited(content.get("inherited", {}), errors)
|
|
|
|
var all_ids: Array[String] = []
|
|
_validate_singleton(content.get("region", {}), "RG-02", all_ids, errors)
|
|
_validate_singleton(content.get("contract", {}), "CTR-R2-01", all_ids, errors)
|
|
_validate_singleton(content.get("contract2", {}), "CTR-R2-02", all_ids, errors)
|
|
var families := {}
|
|
for family in EXPECTED_IDS:
|
|
var rows = content.get(family, [])
|
|
if not rows is Array:
|
|
errors.append("family_type:%s" % family)
|
|
continue
|
|
var by_id := {}
|
|
for row in rows:
|
|
_validate_row(row, String(family), all_ids, by_id, errors)
|
|
families[family] = by_id
|
|
var actual: Array = by_id.keys()
|
|
actual.sort()
|
|
var expected: Array = Array(EXPECTED_IDS[family]).duplicate()
|
|
expected.sort()
|
|
if actual != expected:
|
|
errors.append("family_ids:%s" % family)
|
|
|
|
_validate_contract(content, errors)
|
|
_validate_contract2(content, errors)
|
|
_validate_equipment(families.get("equipment", {}), errors)
|
|
_validate_artifacts(content, families.get("artifacts", {}), errors)
|
|
_validate_bosses(content, families.get("bosses", {}), errors)
|
|
_validate_routes(families.get("routes", {}), errors)
|
|
_validate_contract2_routes(content, all_ids, errors)
|
|
_validate_markets(content, families, errors)
|
|
_validate_contract2_markets(content, errors)
|
|
_validate_catalog_hash(content, errors)
|
|
return {"ok": errors.is_empty(), "errors": errors}
|
|
|
|
|
|
static func rows_by_id(content: Dictionary, family: String) -> Dictionary:
|
|
var result := {}
|
|
for row in content.get(family, []):
|
|
if row is Dictionary:
|
|
result[String(row.get("id", ""))] = row
|
|
return result
|
|
|
|
|
|
static func markets_by_index(content: Dictionary) -> Dictionary:
|
|
var result := {}
|
|
for row in content.get("markets", []):
|
|
if row is Dictionary:
|
|
result[int(row.get("market_index", -1))] = row
|
|
return result
|
|
|
|
|
|
static func _enrich(content: Dictionary) -> void:
|
|
_build_contract2_routes(content)
|
|
for key in ["region", "contract", "contract2"]:
|
|
var singleton = content.get(key, {})
|
|
if singleton is Dictionary:
|
|
_enrich_row(singleton)
|
|
for family in EXPECTED_IDS:
|
|
for row in content.get(family, []):
|
|
if row is Dictionary:
|
|
_enrich_row(row)
|
|
for row in content.get("contract2_routes", []):
|
|
if row is Dictionary:
|
|
_enrich_row(row)
|
|
|
|
|
|
static func _enrich_row(row: Dictionary) -> void:
|
|
var row_id := String(row.get("id", ""))
|
|
row["execution_key"] = "execute.%s" % row_id.to_lower().replace("-", "_")
|
|
row["mechanic_signature"] = _signature(row)
|
|
|
|
|
|
static func _validate_source(source, errors: Array[String]) -> void:
|
|
if not source is Dictionary:
|
|
errors.append("source_type")
|
|
return
|
|
if source.size() != SOURCE_AUTHORITY.size():
|
|
errors.append("source_keys")
|
|
for key in SOURCE_AUTHORITY:
|
|
if String(source.get(key, "")) != String(SOURCE_AUTHORITY[key]):
|
|
errors.append("source:%s" % key)
|
|
|
|
|
|
static func _validate_inherited(inherited, errors: Array[String]) -> void:
|
|
if not inherited is Dictionary:
|
|
errors.append("inherited_type")
|
|
return
|
|
if inherited.size() != EXPECTED_INHERITED.size():
|
|
errors.append("inherited_keys")
|
|
for key in EXPECTED_INHERITED:
|
|
if inherited.get(key, []) != EXPECTED_INHERITED[key]:
|
|
errors.append("inherited:%s" % key)
|
|
|
|
|
|
static func _validate_singleton(
|
|
row,
|
|
expected_id: String,
|
|
all_ids: Array[String],
|
|
errors: Array[String]
|
|
) -> void:
|
|
if not row is Dictionary:
|
|
errors.append("singleton_type:%s" % expected_id)
|
|
return
|
|
_validate_common(row, all_ids, errors)
|
|
if String(row.get("id", "")) != expected_id:
|
|
errors.append("singleton_id:%s" % expected_id)
|
|
|
|
|
|
static func _validate_row(
|
|
row,
|
|
family: String,
|
|
all_ids: Array[String],
|
|
by_id: Dictionary,
|
|
errors: Array[String]
|
|
) -> void:
|
|
if not row is Dictionary:
|
|
errors.append("row_type:%s" % family)
|
|
return
|
|
_validate_common(row, all_ids, errors)
|
|
var row_id := String(row.get("id", ""))
|
|
if by_id.has(row_id):
|
|
errors.append("duplicate_id:%s" % row_id)
|
|
by_id[row_id] = row
|
|
for field in REQUIRED_FIELDS[family]:
|
|
if not row.has(field) or (row[field] is String and String(row[field]).strip_edges().is_empty()):
|
|
errors.append("field:%s:%s" % [row_id, field])
|
|
|
|
|
|
static func _validate_common(
|
|
row: Dictionary,
|
|
all_ids: Array[String],
|
|
errors: Array[String]
|
|
) -> void:
|
|
var row_id := String(row.get("id", ""))
|
|
if row_id.is_empty():
|
|
errors.append("empty_id")
|
|
return
|
|
if row_id in all_ids:
|
|
errors.append("duplicate_global_id:%s" % row_id)
|
|
all_ids.append(row_id)
|
|
if String(row.get("name_en", "")).strip_edges().is_empty():
|
|
errors.append("name_en:%s" % row_id)
|
|
if String(row.get("name_zh", "")).strip_edges().is_empty():
|
|
errors.append("name_zh:%s" % row_id)
|
|
if String(row.get("preview", "")).strip_edges().is_empty():
|
|
errors.append("preview:%s" % row_id)
|
|
var tags = row.get("response_tags", [])
|
|
if not tags is Array or tags.size() < 3 or not _unique(tags):
|
|
errors.append("response_tags:%s" % row_id)
|
|
if String(row.get("execution_key", "")) != "execute.%s" % row_id.to_lower().replace("-", "_"):
|
|
errors.append("execution_key:%s" % row_id)
|
|
if String(row.get("mechanic_signature", "")) != _signature(row):
|
|
errors.append("mechanic_signature:%s" % row_id)
|
|
var prose := JSON.stringify(row).to_lower()
|
|
for term in FILLER_TERMS:
|
|
if prose.contains(term):
|
|
errors.append("filler:%s:%s" % [row_id, term])
|
|
|
|
|
|
static func _validate_contract(content: Dictionary, errors: Array[String]) -> void:
|
|
var contract: Dictionary = content.get("contract", {})
|
|
if (
|
|
String(contract.get("region_id", "")) != "RG-02"
|
|
or int(contract.get("slice_start_market", 0)) != 8
|
|
or int(contract.get("slice_end_market", 0)) != 13
|
|
or String(contract.get("boss_id", "")) != "BO-R2-01"
|
|
):
|
|
errors.append("contract_boundary")
|
|
if not bool(contract.get("boss_executable", false)):
|
|
errors.append("boss_boundary")
|
|
|
|
|
|
static func _validate_contract2(content: Dictionary, errors: Array[String]) -> void:
|
|
var contract: Dictionary = content.get("contract2", {})
|
|
if (
|
|
String(contract.get("region_id", "")) != "RG-02"
|
|
or int(contract.get("slice_start_market", 0)) != 1
|
|
or int(contract.get("slice_end_market", 0)) != 13
|
|
or String(contract.get("boss_id", "")) != "BO-R2-02"
|
|
or bool(contract.get("boss_executable", true))
|
|
):
|
|
errors.append("contract2_boundary")
|
|
|
|
|
|
static func _validate_equipment(equipment: Dictionary, errors: Array[String]) -> void:
|
|
for item_id in equipment:
|
|
var item: Dictionary = equipment[item_id]
|
|
if String(item.get("class", "")) != "B":
|
|
errors.append("equipment_class:%s" % item_id)
|
|
if String(item.get("fit", "")).split(";").size() != 2:
|
|
errors.append("equipment_fit:%s" % item_id)
|
|
|
|
|
|
static func _validate_artifacts(
|
|
content: Dictionary,
|
|
artifacts: Dictionary,
|
|
errors: Array[String]
|
|
) -> void:
|
|
var expected_conflicts := {
|
|
"AR-009": "AR-010",
|
|
"AR-010": "AR-009",
|
|
"AR-013": "AR-014",
|
|
"AR-014": "AR-013",
|
|
}
|
|
for artifact_id in artifacts:
|
|
if String(artifacts[artifact_id].get("conflict_id", "")) != expected_conflicts[artifact_id]:
|
|
errors.append("artifact_conflict:%s" % artifact_id)
|
|
var complete_ids: Array = Array(content.get("inherited", {}).get("artifact_ids", [])).duplicate()
|
|
complete_ids.append_array(artifacts.keys())
|
|
complete_ids.sort()
|
|
if complete_ids != ["AR-009", "AR-010", "AR-011", "AR-012", "AR-013", "AR-014", "AR-015"]:
|
|
errors.append("artifact_complete_pool")
|
|
|
|
|
|
static func _validate_bosses(
|
|
content: Dictionary,
|
|
bosses: Dictionary,
|
|
errors: Array[String]
|
|
) -> void:
|
|
for boss_id in bosses:
|
|
var expected: bool = boss_id == "BO-R2-01"
|
|
if bool(bosses[boss_id].get("executable", false)) != expected:
|
|
errors.append("boss_execution_boundary:%s" % boss_id)
|
|
if String(content.get("contract", {}).get("boss_id", "")) != "BO-R2-01":
|
|
errors.append("boss_contract_reference")
|
|
|
|
|
|
static func _validate_routes(routes: Dictionary, errors: Array[String]) -> void:
|
|
var allowed_enemies: Array = Array(EXPECTED_INHERITED.enemy_ids)
|
|
var enemy_usage := {}
|
|
for enemy_id in allowed_enemies:
|
|
enemy_usage[enemy_id] = 0
|
|
for route_id in routes:
|
|
var route: Dictionary = routes[route_id]
|
|
var market_index := int(route.get("market_index", -1))
|
|
var round_index := int(route.get("round_index", -1))
|
|
var risk := int(route.get("risk", -1))
|
|
if market_index < 8 or market_index > 12 or round_index != market_index:
|
|
errors.append("route_round:%s" % route_id)
|
|
if risk < 0 or risk > 2 or route_id != "RO-R2-M%02d-Q%d" % [market_index, risk]:
|
|
errors.append("route_identity:%s" % route_id)
|
|
var enemy_ids = route.get("enemy_ids", [])
|
|
if not enemy_ids is Array or enemy_ids.size() < 2 or enemy_ids.size() > 3 or not _unique(enemy_ids):
|
|
errors.append("route_composition:%s" % route_id)
|
|
else:
|
|
for enemy_id in enemy_ids:
|
|
if String(enemy_id) not in allowed_enemies:
|
|
errors.append("route_enemy:%s:%s" % [route_id, enemy_id])
|
|
else:
|
|
enemy_usage[String(enemy_id)] += 1
|
|
var waves = route.get("wave_order", [])
|
|
if not waves is Array or waves.size() != enemy_ids.size():
|
|
errors.append("route_waves:%s" % route_id)
|
|
else:
|
|
var wave_ids: Array = []
|
|
var previous_tick := -1
|
|
for wave in waves:
|
|
if not wave is Dictionary:
|
|
errors.append("route_wave_type:%s" % route_id)
|
|
continue
|
|
var tick := int(wave.get("tick", -1))
|
|
wave_ids.append(String(wave.get("enemy_id", "")))
|
|
if tick <= previous_tick:
|
|
errors.append("route_wave_order:%s" % route_id)
|
|
previous_tick = tick
|
|
if wave_ids != enemy_ids:
|
|
errors.append("route_wave_enemy:%s" % route_id)
|
|
var expected_reward := 18 + 2 * round_index + 2 + 7 * risk
|
|
if int(route.get("reward_coin", -1)) != expected_reward:
|
|
errors.append("route_reward:%s" % route_id)
|
|
var recovery := int(route.get("recovery_coin", -1))
|
|
if recovery <= 0 or recovery >= expected_reward:
|
|
errors.append("route_recovery:%s" % route_id)
|
|
for enemy_id in enemy_usage:
|
|
if int(enemy_usage[enemy_id]) < 2:
|
|
errors.append("enemy_underused:%s" % enemy_id)
|
|
|
|
|
|
static func _validate_contract2_routes(
|
|
content: Dictionary,
|
|
all_ids: Array[String],
|
|
errors: Array[String]
|
|
) -> void:
|
|
var rows = content.get("contract2_routes", [])
|
|
if not rows is Array or rows.size() != 36:
|
|
errors.append("contract2_route_count")
|
|
return
|
|
var by_id := {}
|
|
var enemy_usage := {}
|
|
for enemy_id in EXPECTED_INHERITED.enemy_ids:
|
|
enemy_usage[String(enemy_id)] = 0
|
|
for row in rows:
|
|
if not row is Dictionary:
|
|
errors.append("contract2_route_type")
|
|
continue
|
|
_validate_common(row, all_ids, errors)
|
|
var route_id := String(row.get("id", ""))
|
|
if by_id.has(route_id):
|
|
errors.append("contract2_route_duplicate:%s" % route_id)
|
|
by_id[route_id] = row
|
|
for field in [
|
|
"market_index", "round_index", "risk", "enemy_ids", "wave_order",
|
|
"reward_coin", "recovery_coin", "pressure", "failure_cause",
|
|
]:
|
|
if not row.has(field):
|
|
errors.append("contract2_route_field:%s:%s" % [route_id, field])
|
|
var market_index := int(row.get("market_index", -1))
|
|
var risk := int(row.get("risk", -1))
|
|
if route_id != "RO-R2-C02-M%02d-Q%d" % [market_index, risk]:
|
|
errors.append("contract2_route_identity:%s" % route_id)
|
|
if market_index < 1 or market_index > 12 or int(row.get("round_index", -1)) != market_index:
|
|
errors.append("contract2_route_round:%s" % route_id)
|
|
if risk < 0 or risk > 2:
|
|
errors.append("contract2_route_risk:%s" % route_id)
|
|
if int(row.get("reward_coin", -1)) != 20 + 2 * market_index + 7 * risk:
|
|
errors.append("contract2_route_reward:%s" % route_id)
|
|
var recovery := int(row.get("recovery_coin", -1))
|
|
if recovery <= 0 or recovery >= int(row.get("reward_coin", 0)):
|
|
errors.append("contract2_route_recovery:%s" % route_id)
|
|
var enemy_ids = row.get("enemy_ids", [])
|
|
var waves = row.get("wave_order", [])
|
|
if not enemy_ids is Array or enemy_ids.size() < 2 or enemy_ids.size() > 3 or not _unique(enemy_ids):
|
|
errors.append("contract2_route_composition:%s" % route_id)
|
|
elif not waves is Array or waves.size() != enemy_ids.size():
|
|
errors.append("contract2_route_waves:%s" % route_id)
|
|
else:
|
|
for index in range(enemy_ids.size()):
|
|
var enemy_id := String(enemy_ids[index])
|
|
if not enemy_usage.has(enemy_id):
|
|
errors.append("contract2_route_enemy:%s:%s" % [route_id, enemy_id])
|
|
else:
|
|
enemy_usage[enemy_id] += 1
|
|
if String(waves[index].get("enemy_id", "")) != enemy_id:
|
|
errors.append("contract2_route_wave_enemy:%s" % route_id)
|
|
for market_index in range(1, 13):
|
|
for risk in range(3):
|
|
var expected_id := "RO-R2-C02-M%02d-Q%d" % [market_index, risk]
|
|
if not by_id.has(expected_id):
|
|
errors.append("contract2_route_missing:%s" % expected_id)
|
|
for enemy_id in enemy_usage:
|
|
if int(enemy_usage[enemy_id]) < 2:
|
|
errors.append("contract2_enemy_underused:%s" % enemy_id)
|
|
|
|
|
|
static func _validate_markets(
|
|
content: Dictionary,
|
|
families: Dictionary,
|
|
errors: Array[String]
|
|
) -> void:
|
|
var markets = content.get("markets", [])
|
|
if not markets is Array or markets.size() != 5:
|
|
errors.append("market_count")
|
|
return
|
|
var routes: Dictionary = families.get("routes", {})
|
|
var equipment: Dictionary = families.get("equipment", {})
|
|
var allowed_changers: Array = Array(EXPECTED_INHERITED.enemy_ids).duplicate()
|
|
allowed_changers.append("BO-R2-01")
|
|
var seen := {}
|
|
for market in markets:
|
|
if not market is Dictionary:
|
|
errors.append("market_type")
|
|
continue
|
|
var market_index := int(market.get("market_index", -1))
|
|
seen[market_index] = true
|
|
var expected_capacity: int = {8: 7, 9: 8, 10: 8, 11: 9, 12: 9}.get(market_index, -1)
|
|
var expected_level := mini(10, 1 + int(floor(float(market_index - 1) / 3.0)))
|
|
if market_index < 8 or market_index > 12 or int(market.get("round_index", -1)) != market_index:
|
|
errors.append("market_index:%d" % market_index)
|
|
if int(market.get("cleared_rounds_before", -1)) != market_index - 1:
|
|
errors.append("market_lineage:%d" % market_index)
|
|
if int(market.get("line_capacity", -1)) != expected_capacity:
|
|
errors.append("market_capacity:%d" % market_index)
|
|
if int(market.get("offer_level", -1)) != expected_level:
|
|
errors.append("market_offer_level:%d" % market_index)
|
|
var route_ids = market.get("route_ids", [])
|
|
if not route_ids is Array or route_ids.size() != 3:
|
|
errors.append("market_routes:%d" % market_index)
|
|
else:
|
|
for risk in range(3):
|
|
var expected_id := "RO-R2-M%02d-Q%d" % [market_index, risk]
|
|
if String(route_ids[risk]) != expected_id or not routes.has(expected_id):
|
|
errors.append("market_route:%d:%d" % [market_index, risk])
|
|
var required_items = market.get("required_equipment_ids", [])
|
|
if not required_items is Array or required_items.size() != 2 or not _unique(required_items):
|
|
errors.append("market_required_equipment:%d" % market_index)
|
|
else:
|
|
for item_id in required_items:
|
|
if not equipment.has(String(item_id)):
|
|
errors.append("market_equipment:%d:%s" % [market_index, item_id])
|
|
if String(market.get("first_order_changer_id", "")) not in allowed_changers:
|
|
errors.append("market_order_changer:%d" % market_index)
|
|
if String(market.get("milestone_disclosure", "")).strip_edges().is_empty():
|
|
errors.append("market_milestone:%d" % market_index)
|
|
if seen.keys().size() != 5:
|
|
errors.append("market_indices")
|
|
|
|
|
|
static func _validate_contract2_markets(content: Dictionary, errors: Array[String]) -> void:
|
|
var markets = content.get("contract2_markets", [])
|
|
if not markets is Array or markets.size() != 12:
|
|
errors.append("contract2_market_count")
|
|
return
|
|
var equipment := rows_by_id(content, "equipment")
|
|
var allowed_changers: Array = Array(EXPECTED_INHERITED.enemy_ids).duplicate()
|
|
allowed_changers.append("BO-R2-02")
|
|
var seen := {}
|
|
for market in markets:
|
|
if not market is Dictionary:
|
|
errors.append("contract2_market_type")
|
|
continue
|
|
var market_index := int(market.get("market_index", -1))
|
|
seen[market_index] = true
|
|
var expected_level := mini(10, 1 + int(floor(float(market_index - 1) / 3.0)))
|
|
if (
|
|
market_index < 1 or market_index > 12
|
|
or int(market.get("round_index", -1)) != market_index
|
|
or int(market.get("line_capacity", -1)) != 10
|
|
or int(market.get("offer_level", -1)) != expected_level
|
|
):
|
|
errors.append("contract2_market_boundary:%d" % market_index)
|
|
for field in [
|
|
"name_en", "name_zh", "pressure", "failure_cause", "preview",
|
|
"pulse_family", "milestone_disclosure",
|
|
]:
|
|
if String(market.get(field, "")).strip_edges().is_empty():
|
|
errors.append("contract2_market_field:%d:%s" % [market_index, field])
|
|
var tags = market.get("response_tags", [])
|
|
if not tags is Array or tags.size() != 3 or not _unique(tags):
|
|
errors.append("contract2_market_tags:%d" % market_index)
|
|
var enemy_sets = market.get("enemy_sets", [])
|
|
if not enemy_sets is Array or enemy_sets.size() != 3:
|
|
errors.append("contract2_market_enemy_sets:%d" % market_index)
|
|
var items = market.get("required_equipment_ids", [])
|
|
if not items is Array or items.size() != 2 or not _unique(items):
|
|
errors.append("contract2_market_equipment:%d" % market_index)
|
|
else:
|
|
for item_id in items:
|
|
if not equipment.has(String(item_id)):
|
|
errors.append("contract2_market_item:%d:%s" % [market_index, item_id])
|
|
if String(market.get("first_order_changer_id", "")) not in allowed_changers:
|
|
errors.append("contract2_market_changer:%d" % market_index)
|
|
if seen.size() != 12:
|
|
errors.append("contract2_market_indices")
|
|
|
|
|
|
static func _build_contract2_routes(content: Dictionary) -> void:
|
|
var rows: Array[Dictionary] = []
|
|
var suffix_en := ["Survey", "Press", "Hazard"]
|
|
var suffix_zh := ["稳行", "急行", "险行"]
|
|
for market in content.get("contract2_markets", []):
|
|
if not market is Dictionary:
|
|
continue
|
|
var market_index := int(market.get("market_index", -1))
|
|
var enemy_sets = market.get("enemy_sets", [])
|
|
if not enemy_sets is Array or enemy_sets.size() != 3:
|
|
continue
|
|
for risk in range(3):
|
|
var enemy_ids: Array = Array(enemy_sets[risk]).duplicate()
|
|
var waves: Array[Dictionary] = []
|
|
for index in range(enemy_ids.size()):
|
|
waves.append({
|
|
"tick": index * (12 - risk) + (0 if index == 0 else risk),
|
|
"enemy_id": String(enemy_ids[index]),
|
|
})
|
|
rows.append({
|
|
"id": "RO-R2-C02-M%02d-Q%d" % [market_index, risk],
|
|
"name_en": "%s - %s" % [String(market.get("name_en", "")), suffix_en[risk]],
|
|
"name_zh": "%s - %s" % [String(market.get("name_zh", "")), suffix_zh[risk]],
|
|
"market_index": market_index,
|
|
"round_index": market_index,
|
|
"risk": risk,
|
|
"enemy_ids": enemy_ids,
|
|
"wave_order": waves,
|
|
"reward_coin": 20 + 2 * market_index + 7 * risk,
|
|
"recovery_coin": 10 + market_index + 3 * risk,
|
|
"pressure": String(market.get("pressure", "")),
|
|
"failure_cause": String(market.get("failure_cause", "")),
|
|
"preview": String(market.get("preview", "")),
|
|
"response_tags": Array(market.get("response_tags", [])).duplicate(),
|
|
"pulse_family": String(market.get("pulse_family", "")),
|
|
"milestone_disclosure": String(market.get("milestone_disclosure", "")),
|
|
})
|
|
content["contract2_routes"] = rows
|
|
|
|
|
|
static func _validate_catalog_hash(content: Dictionary, errors: Array[String]) -> void:
|
|
var candidate := content.duplicate(true)
|
|
var stored := String(candidate.get("catalog_hash", ""))
|
|
candidate.erase("catalog_hash")
|
|
if stored.length() != 64 or stored != _hash(candidate):
|
|
errors.append("catalog_hash")
|
|
|
|
|
|
static func _signature(row: Dictionary) -> String:
|
|
var projection := row.duplicate(true)
|
|
for key in ["execution_key", "mechanic_signature"]:
|
|
projection.erase(key)
|
|
return _hash(projection)
|
|
|
|
|
|
static func _unique(values: Array) -> bool:
|
|
var seen := {}
|
|
for value in values:
|
|
var key := String(value)
|
|
if seen.has(key):
|
|
return false
|
|
seen[key] = true
|
|
return true
|
|
|
|
|
|
static func _hash(value) -> String:
|
|
var context := HashingContext.new()
|
|
context.start(HashingContext.HASH_SHA256)
|
|
context.update(JSON.stringify(value).to_utf8_buffer())
|
|
return context.finish().hex_encode()
|