class_name SimulationSaveManifest extends RefCounted const SCHEMA_NAME := "the_steward.save_manifest" const SCHEMA_VERSION := 1 const MANIFEST_FIELDS := [ "schema", "schema_version", "local_state", "local_checksum", "regional_state", "regional_checksum", ] const MAX_SAFE_JSON_INTEGER := 9007199254740991.0 var _local_state: SimulationStateRecord var _regional_facade: RegionalSimulationFacade var _local_checksum := "" var _regional_checksum := "" static func create( local_state: SimulationStateRecord, regional_facade: RegionalSimulationFacade ) -> SimulationSaveManifest: if local_state == null or regional_facade == null: return null var local_copy := _canonicalize_local_state(local_state) var regional_copy := _parse_regional_facade(regional_facade.to_dictionary()) if local_copy == null or regional_copy == null: return null return _assemble(local_copy, regional_copy) static func from_dictionary(record_data: Dictionary) -> SimulationSaveManifest: return _from_dictionary(record_data, false) static func _from_dictionary( record_data: Dictionary, allow_json_number_types: bool ) -> SimulationSaveManifest: if not _has_exact_fields(record_data, MANIFEST_FIELDS): return null if ( not record_data["schema"] is String or String(record_data["schema"]) != SCHEMA_NAME or not record_data["schema_version"] is int or int(record_data["schema_version"]) != SCHEMA_VERSION or not record_data["local_state"] is Dictionary or not record_data["local_checksum"] is String or not record_data["regional_state"] is Dictionary or not record_data["regional_checksum"] is String ): return null var local_state := _parse_local_state(record_data["local_state"], allow_json_number_types) var regional_facade := _parse_regional_facade( record_data["regional_state"], allow_json_number_types ) if local_state == null or regional_facade == null: return null var local_checksum := _checksum_dictionary(local_state.to_dictionary()) var regional_checksum := _checksum_dictionary(regional_facade.to_dictionary()) if ( String(record_data["local_checksum"]) != local_checksum or String(record_data["regional_checksum"]) != regional_checksum ): return null return _assemble(local_state, regional_facade) static func from_json(json_text: String) -> SimulationSaveManifest: var parser := JSON.new() if parser.parse(json_text) != OK: return null var parsed: Variant = parser.data if not parsed is Dictionary: return null var normalized: Dictionary = parsed.duplicate(true) if not normalized.has("schema_version"): return null var normalized_version: Variant = _restore_json_integer_types(normalized["schema_version"]) if not normalized_version is int: return null normalized["schema_version"] = normalized_version if normalized.get("local_state") is Dictionary: var normalized_local_state: Variant = _normalize_json_local_boundary_integers( normalized["local_state"] ) if normalized_local_state == null: return null normalized["local_state"] = normalized_local_state if normalized.get("regional_state") is Dictionary: normalized["regional_state"] = _restore_json_integer_types(normalized["regional_state"]) return _from_dictionary(normalized, true) func to_dictionary() -> Dictionary: return { "schema": SCHEMA_NAME, "schema_version": SCHEMA_VERSION, "local_state": _local_state.to_dictionary(), "local_checksum": _local_checksum, "regional_state": _regional_facade.to_dictionary(), "regional_checksum": _regional_checksum, } func to_json() -> String: return JSON.stringify(to_dictionary()) func get_local_state() -> SimulationStateRecord: return _parse_local_state(_local_state.to_dictionary()) func get_regional_facade() -> RegionalSimulationFacade: return _parse_regional_facade(_regional_facade.to_dictionary()) func checksum() -> String: return to_json().sha256_text() static func _assemble( local_state: SimulationStateRecord, regional_facade: RegionalSimulationFacade ) -> SimulationSaveManifest: if not _boundaries_match(local_state, regional_facade): return null var manifest := SimulationSaveManifest.new() manifest._local_state = local_state manifest._regional_facade = regional_facade manifest._local_checksum = _checksum_dictionary(local_state.to_dictionary()) manifest._regional_checksum = _checksum_dictionary(regional_facade.to_dictionary()) return manifest static func _parse_local_state( value: Variant, allow_json_number_types: bool = false ) -> SimulationStateRecord: if not value is Dictionary: return null var source: Dictionary = value var parsed := SimulationStateRecord.from_dictionary(source) if ( parsed == null or not _canonical_tree_matches(parsed.to_dictionary(), source, allow_json_number_types) ): return null return parsed static func _canonicalize_local_state(local_state: SimulationStateRecord) -> SimulationStateRecord: var source := local_state.to_dictionary() var source_simulation: Variant = source.get("simulation") if not source_simulation is Dictionary: return null for field: String in ["seed", "tick_count"]: if not source_simulation.get(field) is int: return null var parser := JSON.new() if parser.parse(JSON.stringify(source)) != OK: return null if not parser.data is Dictionary: return null var canonical_data: Variant = _normalize_json_local_boundary_integers(parser.data) if canonical_data == null: return null var canonical := SimulationStateRecord.from_dictionary(canonical_data) if canonical == null: return null return _parse_local_state(canonical.to_dictionary()) static func _parse_regional_facade( value: Variant, allow_json_number_types: bool = false ) -> RegionalSimulationFacade: if not value is Dictionary: return null var source: Dictionary = value var parsed := RegionalSimulationFacade.from_dictionary(source) if ( parsed == null or not _canonical_tree_matches(parsed.to_dictionary(), source, allow_json_number_types) ): return null return parsed static func _boundaries_match( local_state: SimulationStateRecord, regional_facade: RegionalSimulationFacade ) -> bool: var local_world_id := StringName(local_state.simulation.get("world_id", &"")) var local_location_id := StringName(local_state.simulation.get("location_id", &"")) var local_seed_value: Variant = local_state.simulation.get("seed") var local_tick_value: Variant = local_state.simulation.get("tick_count") if not local_seed_value is int or not local_tick_value is int: return false var local_seed := int(local_seed_value) var local_tick := int(local_tick_value) if local_world_id.is_empty() or local_location_id.is_empty() or local_tick < 0: return false if ( local_seed != regional_facade.get_world_seed() or local_location_id != regional_facade.get_active_location_id() or local_tick != regional_facade.get_last_processed_tick() ): return false var service: RegionalCaravanService = regional_facade.get_service() if service == null: return false var world_state := service.get_world_state() if world_state == null or world_state.get_world_id() != local_world_id: return false var location := world_state.get_location(local_location_id) return location != null and location.get_world_id() == local_world_id static func _checksum_dictionary(value: Dictionary) -> String: return JSON.stringify(_canonical_checksum_value(value)).sha256_text() static func _canonical_checksum_value(value: Variant) -> Variant: if _is_number(value): var number := float(value) if is_finite(number) and number == floorf(number) and absf(number) <= MAX_SAFE_JSON_INTEGER: return int(number) return number match typeof(value): TYPE_ARRAY: var normalized_array: Array = [] for item: Variant in value: normalized_array.append(_canonical_checksum_value(item)) return normalized_array TYPE_DICTIONARY: var normalized_dictionary: Dictionary = {} for key: Variant in value: normalized_dictionary[key] = _canonical_checksum_value(value[key]) return normalized_dictionary return value static func _has_exact_fields(value: Dictionary, fields: Array) -> bool: return value.size() == fields.size() and value.has_all(fields) static func _canonical_tree_matches( canonical: Variant, source: Variant, allow_json_number_types: bool ) -> bool: if allow_json_number_types and _is_number(canonical) and _is_number(source): return float(canonical) == float(source) if typeof(canonical) != typeof(source): return false match typeof(canonical): TYPE_ARRAY: if canonical.size() != source.size(): return false for index in canonical.size(): if not _canonical_tree_matches( canonical[index], source[index], allow_json_number_types ): return false return true TYPE_DICTIONARY: if canonical.size() != source.size(): return false for key: Variant in canonical: if ( not source.has(key) or not _canonical_tree_matches( canonical[key], source[key], allow_json_number_types ) ): return false return true return canonical == source static func _is_number(value: Variant) -> bool: return value is int or value is float static func _normalize_json_local_boundary_integers(value: Dictionary) -> Variant: var normalized := value.duplicate(true) var simulation: Variant = normalized.get("simulation") if not simulation is Dictionary: return normalized for field: String in ["seed", "tick_count"]: if not simulation.has(field): continue var normalized_value: Variant = _restore_json_integer_types(simulation[field]) if not normalized_value is int: return null simulation[field] = normalized_value normalized["simulation"] = simulation return normalized static func _restore_json_integer_types(value: Variant) -> Variant: match typeof(value): TYPE_FLOAT: var number := float(value) if ( is_finite(number) and number == floorf(number) and absf(number) <= MAX_SAFE_JSON_INTEGER ): return int(number) return number TYPE_ARRAY: var normalized_array: Array = [] for item: Variant in value: normalized_array.append(_restore_json_integer_types(item)) return normalized_array TYPE_DICTIONARY: var normalized_dictionary: Dictionary = {} for key: Variant in value: var normalized_value: Variant = _restore_json_integer_types(value[key]) if String(key).ends_with("_ledger") and normalized_value is Dictionary: for ledger_key: Variant in normalized_value: if not _is_number(normalized_value[ledger_key]): return null normalized_value[ledger_key] = float(normalized_value[ledger_key]) normalized_dictionary[key] = normalized_value return normalized_dictionary return value