639 lines
23 KiB
GDScript
639 lines
23 KiB
GDScript
class_name FG3RunState
|
|
extends RefCounted
|
|
|
|
const P103State = preload("res://p10_3/run_state.gd")
|
|
const MarketGenerator = preload("res://fg3/contract1_market_generator.gd")
|
|
const Contract2MarketGenerator = preload("res://fg3/contract2_market_generator.gd")
|
|
|
|
const STATE_KIND := "fg3_complete_rg02"
|
|
const RULESET := "fg3-complete-rg02-r1"
|
|
const PHASES := [
|
|
"market", "prepared", "recovery", "boss_muster", "boss_prepared",
|
|
"boss_recovery", "contract_result", "contract2_handoff", "contract2_market",
|
|
"contract2_prepared", "contract2_recovery", "contract2_boss_muster",
|
|
"contract2_boss_prepared", "contract2_boss_recovery", "region_result",
|
|
"region_closed",
|
|
]
|
|
|
|
var state_kind := STATE_KIND
|
|
var ruleset := RULESET
|
|
var revision := 0
|
|
var lineage_id := ""
|
|
var generation_root_seed := ""
|
|
var next_region_run_ordinal := 0
|
|
var migration_source_hash := ""
|
|
var migration_source_revision := 0
|
|
var guild_profile: Dictionary = {}
|
|
var current_run: Dictionary = {}
|
|
var operation_receipts: Dictionary = {}
|
|
var legacy_operation_receipts: Dictionary = {}
|
|
var legacy_entry_commits: Dictionary = {}
|
|
var battle_checkpoints: Dictionary = {}
|
|
|
|
|
|
func initialize_from_p103(p103_snapshot: Dictionary, market_board: Dictionary) -> bool:
|
|
var source_probe := P103State.new()
|
|
if not source_probe.restore_snapshot(p103_snapshot):
|
|
return false
|
|
var source := source_probe.snapshot()
|
|
var prior: Dictionary = source.current_run
|
|
if (
|
|
String(prior.phase) != "market_8"
|
|
or int(prior.market_index) != 8
|
|
or int(prior.cleared_rounds) != 7
|
|
or String(market_board.get("run_seed", "")) != String(prior.run_seed)
|
|
or int(market_board.get("market_index", -1)) != 8
|
|
or int(market_board.get("refresh_index", -1)) != 0
|
|
):
|
|
return false
|
|
state_kind = STATE_KIND
|
|
ruleset = RULESET
|
|
revision = 0
|
|
lineage_id = String(source.lineage_id)
|
|
generation_root_seed = String(source.generation_root_seed)
|
|
next_region_run_ordinal = int(source.next_region_run_ordinal)
|
|
migration_source_hash = _hash(source)
|
|
migration_source_revision = int(source.revision)
|
|
guild_profile = Dictionary(source.guild_profile).duplicate(true)
|
|
operation_receipts = {}
|
|
legacy_operation_receipts = Dictionary(source.legacy_operation_receipts).duplicate(true)
|
|
for operation_id in source.operation_receipts:
|
|
if legacy_operation_receipts.has(operation_id):
|
|
return false
|
|
legacy_operation_receipts[operation_id] = Dictionary(source.operation_receipts[operation_id]).duplicate(true)
|
|
legacy_entry_commits = Dictionary(source.legacy_entry_commits).duplicate(true)
|
|
battle_checkpoints = {}
|
|
current_run = prior.duplicate(true)
|
|
current_run.phase = "market"
|
|
current_run.market_index = 8
|
|
current_run.round_index = 8
|
|
current_run.cleared_rounds = 7
|
|
current_run.line_capacity = 7
|
|
current_run.refresh_count = 0
|
|
current_run.market_board = market_board.duplicate(true)
|
|
current_run.available_recruit_offer_ids = _offer_ids(market_board.recruit_slots, "slot_id")
|
|
current_run.available_equipment_offer_ids = _offer_ids(market_board.equipment_offers, "offer_id")
|
|
current_run.available_battle_offer_ids = _offer_ids(market_board.battle_offers, "offer_id")
|
|
current_run.locked_offer_ids = []
|
|
current_run.selected_route_id = ""
|
|
current_run.pending_encounter = {}
|
|
current_run.pre_battle_checkpoint_id = ""
|
|
current_run.recovery_required = false
|
|
current_run.recovery_cost = 0
|
|
current_run.boss_muster = {}
|
|
current_run.selected_boss_plan = ""
|
|
current_run.pending_boss_result = {}
|
|
current_run.next_contract_id = ""
|
|
current_run.fg3_activated = true
|
|
return invariants_hold()
|
|
|
|
|
|
func snapshot() -> Dictionary:
|
|
var raw := {
|
|
"state_kind": state_kind,
|
|
"ruleset": ruleset,
|
|
"revision": revision,
|
|
"lineage_id": lineage_id,
|
|
"generation_root_seed": generation_root_seed,
|
|
"next_region_run_ordinal": next_region_run_ordinal,
|
|
"migration_source_hash": migration_source_hash,
|
|
"migration_source_revision": migration_source_revision,
|
|
"guild_profile": guild_profile.duplicate(true),
|
|
"current_run": current_run.duplicate(true),
|
|
"operation_receipts": operation_receipts.duplicate(true),
|
|
"legacy_operation_receipts": legacy_operation_receipts.duplicate(true),
|
|
"legacy_entry_commits": legacy_entry_commits.duplicate(true),
|
|
"battle_checkpoints": battle_checkpoints.duplicate(true),
|
|
}
|
|
var normalized = JSON.parse_string(JSON.stringify(raw))
|
|
return Dictionary(normalized).duplicate(true) if normalized is Dictionary else {}
|
|
|
|
|
|
func restore_snapshot(raw: Dictionary) -> bool:
|
|
var normalized_value = JSON.parse_string(JSON.stringify(raw))
|
|
if not normalized_value is Dictionary:
|
|
return false
|
|
var normalized: Dictionary = normalized_value
|
|
var required := [
|
|
"state_kind", "ruleset", "revision", "lineage_id",
|
|
"generation_root_seed", "next_region_run_ordinal",
|
|
"migration_source_hash", "migration_source_revision", "guild_profile",
|
|
"current_run", "operation_receipts", "legacy_operation_receipts",
|
|
"legacy_entry_commits", "battle_checkpoints",
|
|
]
|
|
for key in required:
|
|
if not normalized.has(key):
|
|
return false
|
|
for key in [
|
|
"guild_profile", "current_run", "operation_receipts",
|
|
"legacy_operation_receipts", "legacy_entry_commits", "battle_checkpoints",
|
|
]:
|
|
if not normalized[key] is Dictionary:
|
|
return false
|
|
var had_valid_before := invariants_hold()
|
|
var before := snapshot() if had_valid_before else {}
|
|
_assign(normalized)
|
|
if not invariants_hold():
|
|
if had_valid_before:
|
|
_assign(before)
|
|
return false
|
|
return true
|
|
|
|
|
|
func invariant_report() -> Dictionary:
|
|
var phase := String(current_run.get("phase", ""))
|
|
var run_holds: bool = _run_holds()
|
|
var company_fields := [
|
|
"company", "deployed_recruit_ids", "reserve_recruit_ids",
|
|
"recruit_records", "inventory", "equipped_by_recruit",
|
|
]
|
|
var company_ready: bool = company_fields.all(func(key): return current_run.has(key))
|
|
return {
|
|
"state_identity": state_kind == STATE_KIND and ruleset == RULESET and revision >= 0,
|
|
"lineage": _is_hex(lineage_id, 32) and _is_hex(generation_root_seed, 32),
|
|
"migration": _is_hex(migration_source_hash, 64) and migration_source_revision >= 0,
|
|
"guild": _guild_holds(),
|
|
"receipts": _receipts_hold(),
|
|
"company": _company_holds() if company_ready else false,
|
|
"checkpoints": _checkpoints_hold(),
|
|
"phase": phase,
|
|
"phase_holds": run_holds,
|
|
"run": run_holds,
|
|
}
|
|
|
|
|
|
func invariants_hold() -> bool:
|
|
if state_kind != STATE_KIND or ruleset != RULESET or revision < 0:
|
|
return false
|
|
if not _is_hex(lineage_id, 32) or not _is_hex(generation_root_seed, 32):
|
|
return false
|
|
if not _is_hex(migration_source_hash, 64) or migration_source_revision < 0:
|
|
return false
|
|
if next_region_run_ordinal < 0 or not _guild_holds() or not _receipts_hold():
|
|
return false
|
|
return _run_holds()
|
|
|
|
|
|
func _guild_holds() -> bool:
|
|
for key in [
|
|
"schema", "persistent_renown", "contract_clear_ids", "discovery_ids",
|
|
"company_records", "unlocked_region_ids", "clear_receipts",
|
|
]:
|
|
if not guild_profile.has(key):
|
|
return false
|
|
for key in ["contract_clear_ids", "discovery_ids", "unlocked_region_ids"]:
|
|
if not guild_profile[key] is Array or not _unique(guild_profile[key]):
|
|
return false
|
|
if not guild_profile.company_records is Array:
|
|
return false
|
|
var record_ids: Array = []
|
|
for record in guild_profile.company_records:
|
|
if not record is Dictionary or String(record.get("record_id", "")).is_empty():
|
|
return false
|
|
record_ids.append(String(record.record_id))
|
|
if not _unique(record_ids):
|
|
return false
|
|
return (
|
|
guild_profile.clear_receipts is Dictionary
|
|
and int(guild_profile.persistent_renown) >= 0
|
|
and "RG-02" in guild_profile.unlocked_region_ids
|
|
and "CLEAR-RG01-C01" in guild_profile.contract_clear_ids
|
|
and "CLEAR-RG01-C02" in guild_profile.contract_clear_ids
|
|
)
|
|
|
|
|
|
func _run_holds() -> bool:
|
|
var required := [
|
|
"run_id", "region_id", "contract_id", "difficulty", "run_seed",
|
|
"status", "phase", "market_index", "round_index", "cleared_rounds",
|
|
"coin", "replacement_credit", "refresh_count", "line_capacity",
|
|
"market_board", "available_recruit_offer_ids",
|
|
"available_equipment_offer_ids", "available_battle_offer_ids",
|
|
"locked_offer_ids", "company", "deployed_recruit_ids",
|
|
"reserve_recruit_ids", "recruit_records", "inventory",
|
|
"equipped_by_recruit", "formation_proposal", "selected_route_id",
|
|
"pending_encounter", "pre_battle_checkpoint_id", "recovery_required",
|
|
"recovery_cost", "last_battle_report", "route_history",
|
|
"recruits_hired_total", "recruits_fallen_total",
|
|
"recruits_dismissed_total", "active_artifact_ids",
|
|
"artifact_rule_state", "artifact_draft", "promotion_draft",
|
|
"boss_muster", "selected_boss_plan", "pending_boss_result",
|
|
"next_contract_id", "fg3_activated",
|
|
]
|
|
for key in required:
|
|
if not current_run.has(key):
|
|
return false
|
|
for key in [
|
|
"market_board", "recruit_records", "inventory", "equipped_by_recruit",
|
|
"pending_encounter", "last_battle_report", "artifact_rule_state",
|
|
"artifact_draft", "promotion_draft", "boss_muster",
|
|
"pending_boss_result",
|
|
]:
|
|
if not current_run[key] is Dictionary:
|
|
return false
|
|
for key in [
|
|
"available_recruit_offer_ids", "available_equipment_offer_ids",
|
|
"available_battle_offer_ids", "locked_offer_ids", "company",
|
|
"deployed_recruit_ids", "reserve_recruit_ids", "formation_proposal",
|
|
"route_history", "active_artifact_ids",
|
|
]:
|
|
if not current_run[key] is Array:
|
|
return false
|
|
if (
|
|
not bool(current_run.fg3_activated)
|
|
or String(current_run.region_id) != "RG-02"
|
|
or String(current_run.phase) not in PHASES
|
|
or not _is_hex(String(current_run.run_id), 32)
|
|
or not _is_hex(String(current_run.run_seed), 32)
|
|
):
|
|
return false
|
|
var phase := String(current_run.phase)
|
|
var expected_status := "closed" if phase == "region_closed" else "active"
|
|
if String(current_run.status) != expected_status:
|
|
return false
|
|
if int(current_run.coin) < 0 or int(current_run.replacement_credit) < 0:
|
|
return false
|
|
if current_run.route_history.size() != int(current_run.cleared_rounds):
|
|
return false
|
|
var contract2_phase := (
|
|
phase.begins_with("contract2_")
|
|
and phase != "contract2_handoff"
|
|
) or phase in ["region_result", "region_closed"]
|
|
var expected_contract := "CTR-R2-02" if contract2_phase else "CTR-R2-01"
|
|
if String(current_run.contract_id) != expected_contract:
|
|
return false
|
|
if not _company_holds() or not _checkpoints_hold():
|
|
return false
|
|
if phase in ["contract2_market", "contract2_prepared", "contract2_recovery"]:
|
|
return _contract2_ordinary_holds(phase)
|
|
if phase in [
|
|
"contract2_boss_muster", "contract2_boss_prepared",
|
|
"contract2_boss_recovery", "region_result",
|
|
]:
|
|
return _contract2_boss_holds(phase)
|
|
if phase == "region_closed":
|
|
return _region_closed_holds()
|
|
if phase in ["market", "prepared", "recovery"]:
|
|
return _ordinary_phase_holds(phase)
|
|
if phase in ["boss_muster", "boss_prepared", "boss_recovery", "contract_result"]:
|
|
return _boss_phase_holds(phase)
|
|
return _handoff_holds()
|
|
|
|
|
|
func _ordinary_phase_holds(phase: String) -> bool:
|
|
var market_index := int(current_run.market_index)
|
|
var expected_capacity: int = {8: 7, 9: 8, 10: 8, 11: 9, 12: 9}.get(market_index, -1)
|
|
if (
|
|
market_index < 8 or market_index > 12
|
|
or int(current_run.round_index) != market_index
|
|
or int(current_run.cleared_rounds) != market_index - 1
|
|
or int(current_run.line_capacity) != expected_capacity
|
|
):
|
|
return false
|
|
var generated := MarketGenerator.new().generate(
|
|
String(current_run.run_seed), market_index, int(current_run.refresh_count)
|
|
)
|
|
if not generated.ok or _hash(current_run.market_board) != _hash(generated.board):
|
|
return false
|
|
var recruit_ids := _offer_ids(current_run.market_board.recruit_slots, "slot_id")
|
|
var equipment_ids := _offer_ids(current_run.market_board.equipment_offers, "offer_id")
|
|
var battle_ids := _offer_ids(current_run.market_board.battle_offers, "offer_id")
|
|
if (
|
|
current_run.available_recruit_offer_ids != recruit_ids
|
|
or current_run.available_equipment_offer_ids != equipment_ids
|
|
or current_run.available_battle_offer_ids != battle_ids
|
|
):
|
|
return false
|
|
var checkpoint_id := String(current_run.pre_battle_checkpoint_id)
|
|
if phase == "market":
|
|
return (
|
|
String(current_run.selected_route_id).is_empty()
|
|
and current_run.pending_encounter.is_empty()
|
|
and checkpoint_id.is_empty()
|
|
and not bool(current_run.recovery_required)
|
|
and int(current_run.recovery_cost) == 0
|
|
)
|
|
if phase == "prepared":
|
|
return (
|
|
String(current_run.selected_route_id) in battle_ids
|
|
and String(current_run.pending_encounter.get("offer_id", "")) == String(current_run.selected_route_id)
|
|
and checkpoint_id in battle_checkpoints
|
|
and not bool(current_run.recovery_required)
|
|
)
|
|
return (
|
|
bool(current_run.recovery_required)
|
|
and int(current_run.recovery_cost) > 0
|
|
and String(current_run.selected_route_id).is_empty()
|
|
and current_run.pending_encounter.is_empty()
|
|
and checkpoint_id in battle_checkpoints
|
|
and not current_run.last_battle_report.is_empty()
|
|
)
|
|
|
|
|
|
func _boss_phase_holds(phase: String) -> bool:
|
|
if (
|
|
int(current_run.market_index) != 13
|
|
or int(current_run.round_index) != 13
|
|
or int(current_run.cleared_rounds) != 12
|
|
or int(current_run.line_capacity) != 10
|
|
or current_run.boss_muster.is_empty()
|
|
or String(current_run.boss_muster.get("boss_id", "")) != "BO-R2-01"
|
|
or String(current_run.boss_muster.get("muster_hash", "")) != _muster_hash(current_run.boss_muster)
|
|
):
|
|
return false
|
|
var checkpoint_id := String(current_run.pre_battle_checkpoint_id)
|
|
if phase == "boss_muster":
|
|
return String(current_run.selected_boss_plan).is_empty() and checkpoint_id.is_empty()
|
|
if phase == "boss_prepared":
|
|
return (
|
|
String(current_run.selected_boss_plan) in current_run.boss_muster.plan_ids
|
|
and checkpoint_id in battle_checkpoints
|
|
and not bool(current_run.recovery_required)
|
|
)
|
|
if phase == "boss_recovery":
|
|
return (
|
|
bool(current_run.recovery_required)
|
|
and int(current_run.recovery_cost) > 0
|
|
and checkpoint_id in battle_checkpoints
|
|
and not current_run.pending_boss_result.is_empty()
|
|
)
|
|
return (
|
|
not current_run.pending_boss_result.is_empty()
|
|
and String(current_run.pending_boss_result.get("outcome", "")) == "victory"
|
|
and checkpoint_id.is_empty()
|
|
)
|
|
|
|
|
|
func _handoff_holds() -> bool:
|
|
if (
|
|
int(current_run.market_index) != 13
|
|
or int(current_run.cleared_rounds) != 12
|
|
or String(current_run.next_contract_id) != "CTR-R2-02"
|
|
or String(current_run.market_board.get("handoff_kind", "")) != "fg3.contract2_handoff"
|
|
or String(current_run.market_board.get("handoff_hash", "")) != _handoff_hash(current_run.market_board)
|
|
):
|
|
return false
|
|
return (
|
|
"CLEAR-RG02-C01" in guild_profile.contract_clear_ids
|
|
and guild_profile.clear_receipts.has("CLEAR-RG02-C01")
|
|
and guild_profile.company_records.any(
|
|
func(record): return String(record.get("contract_id", "")) == "CTR-R2-01"
|
|
)
|
|
)
|
|
|
|
|
|
func _contract2_ordinary_holds(phase: String) -> bool:
|
|
var market_index := int(current_run.market_index)
|
|
if (
|
|
market_index < 1 or market_index > 12
|
|
or int(current_run.round_index) != market_index
|
|
or int(current_run.cleared_rounds) != 12 + market_index - 1
|
|
or int(current_run.line_capacity) != 10
|
|
or int(current_run.refresh_count) < 0
|
|
or String(current_run.next_contract_id) != ""
|
|
or not current_run.boss_muster.is_empty()
|
|
or not current_run.pending_boss_result.is_empty()
|
|
):
|
|
return false
|
|
var generated := Contract2MarketGenerator.new().generate(
|
|
String(current_run.run_seed),
|
|
market_index,
|
|
int(current_run.refresh_count)
|
|
)
|
|
if not generated.ok or _hash(current_run.market_board) != _hash(generated.board):
|
|
return false
|
|
if (
|
|
current_run.available_recruit_offer_ids != _offer_ids(current_run.market_board.recruit_slots, "slot_id")
|
|
or current_run.available_equipment_offer_ids != _offer_ids(current_run.market_board.equipment_offers, "offer_id")
|
|
or current_run.available_battle_offer_ids != _offer_ids(current_run.market_board.battle_offers, "offer_id")
|
|
or not current_run.locked_offer_ids.is_empty()
|
|
):
|
|
return false
|
|
var checkpoint_id := String(current_run.pre_battle_checkpoint_id)
|
|
if phase == "contract2_market":
|
|
return (
|
|
String(current_run.selected_route_id).is_empty()
|
|
and current_run.pending_encounter.is_empty()
|
|
and checkpoint_id.is_empty()
|
|
and not bool(current_run.recovery_required)
|
|
and int(current_run.recovery_cost) == 0
|
|
)
|
|
if phase == "contract2_prepared":
|
|
return (
|
|
String(current_run.selected_route_id) in current_run.available_battle_offer_ids
|
|
and String(current_run.pending_encounter.get("offer_id", "")) == String(current_run.selected_route_id)
|
|
and checkpoint_id in battle_checkpoints
|
|
and not bool(current_run.recovery_required)
|
|
)
|
|
return (
|
|
bool(current_run.recovery_required)
|
|
and int(current_run.recovery_cost) > 0
|
|
and String(current_run.selected_route_id).is_empty()
|
|
and current_run.pending_encounter.is_empty()
|
|
and checkpoint_id in battle_checkpoints
|
|
and not current_run.last_battle_report.is_empty()
|
|
)
|
|
|
|
|
|
func _contract2_boss_holds(phase: String) -> bool:
|
|
if (
|
|
int(current_run.market_index) != 13
|
|
or int(current_run.round_index) != 13
|
|
or int(current_run.cleared_rounds) != 24
|
|
or int(current_run.line_capacity) != 10
|
|
or current_run.boss_muster.is_empty()
|
|
or String(current_run.boss_muster.get("boss_id", "")) != "BO-R2-02"
|
|
or String(current_run.boss_muster.get("muster_hash", "")) != _muster_hash(current_run.boss_muster)
|
|
):
|
|
return false
|
|
var checkpoint_id := String(current_run.pre_battle_checkpoint_id)
|
|
if phase == "contract2_boss_muster":
|
|
return String(current_run.selected_boss_plan).is_empty() and checkpoint_id.is_empty()
|
|
if phase == "contract2_boss_prepared":
|
|
return (
|
|
String(current_run.selected_boss_plan) in current_run.boss_muster.plan_ids
|
|
and checkpoint_id in battle_checkpoints
|
|
and not bool(current_run.recovery_required)
|
|
)
|
|
if phase == "contract2_boss_recovery":
|
|
return (
|
|
bool(current_run.recovery_required)
|
|
and int(current_run.recovery_cost) > 0
|
|
and checkpoint_id in battle_checkpoints
|
|
and not current_run.pending_boss_result.is_empty()
|
|
)
|
|
return (
|
|
not current_run.pending_boss_result.is_empty()
|
|
and String(current_run.pending_boss_result.get("outcome", "")) == "victory"
|
|
and checkpoint_id.is_empty()
|
|
)
|
|
|
|
|
|
func _region_closed_holds() -> bool:
|
|
if (
|
|
int(current_run.market_index) != 13
|
|
or int(current_run.cleared_rounds) != 24
|
|
or String(current_run.market_board.get("result_kind", "")) != "fg3.rg02_closed"
|
|
or String(current_run.market_board.get("result_hash", "")) != _result_hash(current_run.market_board)
|
|
):
|
|
return false
|
|
return (
|
|
"CLEAR-RG02-C01" in guild_profile.contract_clear_ids
|
|
and "CLEAR-RG02-C02" in guild_profile.contract_clear_ids
|
|
and guild_profile.clear_receipts.has("CLEAR-RG02-C02")
|
|
and "RG-03" in guild_profile.unlocked_region_ids
|
|
and guild_profile.company_records.any(
|
|
func(record): return String(record.get("contract_id", "")) == "CTR-R2-02"
|
|
)
|
|
)
|
|
|
|
|
|
func _company_holds() -> bool:
|
|
if current_run.company.is_empty() or not _unique(current_run.company):
|
|
return false
|
|
if not _unique(current_run.deployed_recruit_ids) or not _subset(current_run.deployed_recruit_ids, current_run.company):
|
|
return false
|
|
var expected_reserve: Array = []
|
|
for recruit_id in current_run.company:
|
|
if not current_run.recruit_records.has(recruit_id) or not current_run.equipped_by_recruit.has(recruit_id):
|
|
return false
|
|
var record: Dictionary = current_run.recruit_records[recruit_id]
|
|
if int(record.get("current_hp", 0)) <= 0 or int(record.get("current_hp", 0)) > int(record.get("max_hp", 0)):
|
|
return false
|
|
var equipped_id := String(current_run.equipped_by_recruit[recruit_id])
|
|
if not equipped_id.is_empty():
|
|
if not current_run.inventory.has(equipped_id):
|
|
return false
|
|
if String(current_run.inventory[equipped_id].get("wearer_id", "")) != recruit_id:
|
|
return false
|
|
if recruit_id not in current_run.deployed_recruit_ids:
|
|
expected_reserve.append(recruit_id)
|
|
if current_run.reserve_recruit_ids != expected_reserve:
|
|
return false
|
|
for recruit_id in current_run.recruit_records:
|
|
if recruit_id not in current_run.company:
|
|
return false
|
|
for instance_id in current_run.inventory:
|
|
var item = current_run.inventory[instance_id]
|
|
if not item is Dictionary:
|
|
return false
|
|
var wearer := String(item.get("wearer_id", ""))
|
|
if not wearer.is_empty() and wearer not in current_run.company:
|
|
return false
|
|
return true
|
|
|
|
|
|
func _receipts_hold() -> bool:
|
|
for operation_id in operation_receipts:
|
|
var record = operation_receipts[operation_id]
|
|
if (
|
|
String(operation_id).is_empty()
|
|
or not record is Dictionary
|
|
or not _is_hex(String(record.get("intent_hash", "")), 64)
|
|
or not record.get("receipt", {}) is Dictionary
|
|
):
|
|
return false
|
|
return true
|
|
|
|
|
|
func _checkpoints_hold() -> bool:
|
|
for checkpoint_id in battle_checkpoints:
|
|
var checkpoint = battle_checkpoints[checkpoint_id]
|
|
if (
|
|
not _is_hex(String(checkpoint_id), 64)
|
|
or not checkpoint is Dictionary
|
|
or String(checkpoint.get("checkpoint_id", "")) != checkpoint_id
|
|
or not checkpoint.get("source_snapshot", {}) is Dictionary
|
|
):
|
|
return false
|
|
return true
|
|
|
|
|
|
func _assign(raw: Dictionary) -> void:
|
|
state_kind = String(raw.state_kind)
|
|
ruleset = String(raw.ruleset)
|
|
revision = int(raw.revision)
|
|
lineage_id = String(raw.lineage_id)
|
|
generation_root_seed = String(raw.generation_root_seed)
|
|
next_region_run_ordinal = int(raw.next_region_run_ordinal)
|
|
migration_source_hash = String(raw.migration_source_hash)
|
|
migration_source_revision = int(raw.migration_source_revision)
|
|
guild_profile = Dictionary(raw.guild_profile).duplicate(true)
|
|
current_run = Dictionary(raw.current_run).duplicate(true)
|
|
operation_receipts = Dictionary(raw.operation_receipts).duplicate(true)
|
|
legacy_operation_receipts = Dictionary(raw.legacy_operation_receipts).duplicate(true)
|
|
legacy_entry_commits = Dictionary(raw.legacy_entry_commits).duplicate(true)
|
|
battle_checkpoints = Dictionary(raw.battle_checkpoints).duplicate(true)
|
|
|
|
|
|
func _offer_ids(offers: Array, field: String) -> Array[String]:
|
|
var result: Array[String] = []
|
|
for offer in offers:
|
|
result.append(String(offer.get(field, "")))
|
|
return result
|
|
|
|
|
|
func _muster_hash(muster: Dictionary) -> String:
|
|
var projection := muster.duplicate(true)
|
|
projection.erase("muster_hash")
|
|
return _hash(projection)
|
|
|
|
|
|
func _handoff_hash(handoff: Dictionary) -> String:
|
|
var projection := handoff.duplicate(true)
|
|
projection.erase("handoff_hash")
|
|
return _hash(projection)
|
|
|
|
|
|
func _result_hash(result: Dictionary) -> String:
|
|
var projection := result.duplicate(true)
|
|
projection.erase("result_hash")
|
|
return _hash(projection)
|
|
|
|
|
|
func _subset(values: Array, authority: Array) -> bool:
|
|
for value in values:
|
|
if value not in authority:
|
|
return false
|
|
return true
|
|
|
|
|
|
func _unique(values: Array) -> bool:
|
|
var seen := {}
|
|
for value in values:
|
|
if seen.has(value):
|
|
return false
|
|
seen[value] = true
|
|
return true
|
|
|
|
|
|
func _is_hex(value: String, length: int) -> bool:
|
|
if value.length() != length:
|
|
return false
|
|
for character in value:
|
|
if character not in "0123456789abcdef":
|
|
return false
|
|
return true
|
|
|
|
|
|
func _hash(value) -> String:
|
|
return JSON.stringify(_canonical(value)).sha256_text()
|
|
|
|
|
|
func _canonical(value):
|
|
if value is float and is_equal_approx(value, round(value)):
|
|
return int(round(value))
|
|
if value is Dictionary:
|
|
var result := {}
|
|
var keys: Array = value.keys()
|
|
keys.sort_custom(func(left, right): return String(left) < String(right))
|
|
for key in keys:
|
|
result[key] = _canonical(value[key])
|
|
return result
|
|
if value is Array:
|
|
var result: Array = []
|
|
for item in value:
|
|
result.append(_canonical(item))
|
|
return result
|
|
return value
|