class_name SimulationSnapshot extends RefCounted const SCHEMA_VERSION := 1 static func capture(clock: SimulationKernel, state: Node, extra := {}) -> Dictionary: var snapshot := { &"schema_version": SCHEMA_VERSION, &"clock": clock.clock_snapshot(), &"game_state": state.create_snapshot(), } if extra is Dictionary and not extra.is_empty(): snapshot[&"systems"] = extra.duplicate(true) return snapshot static func validate(snapshot: Dictionary) -> Dictionary: var errors: Array[String] = [] var schema_value: Variant = snapshot.get(&"schema_version", null) if not _is_nonnegative_integer(schema_value) or int(schema_value) != SCHEMA_VERSION: errors.append("unsupported_schema_version") if not snapshot.get(&"clock", null) is Dictionary: errors.append("missing_clock") else: var clock_data: Dictionary = snapshot[&"clock"] if not _is_nonnegative_integer(clock_data.get(&"absolute_minute", null)): errors.append("invalid_absolute_minute") var speed_value: Variant = clock_data.get(&"speed", null) if not _is_nonnegative_integer(speed_value) or int(speed_value) not in [1, 2, 3, 4]: errors.append("invalid_speed") # Legacy envelopes may omit these fields and retain restore_clock's # paused=true/version=0 defaults. Present fields cannot be coerced. if typeof(clock_data.get(&"paused", true)) != TYPE_BOOL: errors.append("invalid_paused") if not _is_nonnegative_integer(clock_data.get(&"snapshot_version", 0)): errors.append("invalid_snapshot_version") if not snapshot.get(&"game_state", null) is Dictionary: errors.append("missing_game_state") return { &"ok": errors.is_empty(), &"errors": errors, } static func _is_nonnegative_integer(value: Variant) -> bool: if typeof(value) == TYPE_INT: return value >= 0 # JSON numbers are floats, but exact integral values remain compatible. # Avoid an overflowing float-to-int conversion at the signed int64 limit. if typeof(value) != TYPE_FLOAT or not is_finite(value): return false return value >= 0 and value < 9223372036854775808.0 and value == floor(value) static func restore(snapshot: Dictionary, clock: SimulationKernel, state: Node) -> bool: var validation := validate(snapshot) if not validation[&"ok"]: return false var previous_clock := clock.clock_snapshot() var previous_state: Dictionary = state.create_snapshot() if not clock.restore_clock(snapshot[&"clock"]): return false if not state.restore_snapshot(snapshot[&"game_state"]): clock.restore_clock(previous_clock) state.restore_snapshot(previous_state) return false return true