class_name SaveService extends RefCounted const FORMAT_VERSION := 1 const MAGIC := "TTWB" const WORLD_FILE := "world.bin" const MANIFEST_FILE := "manifest.json" const THUMBNAIL_FILE := "thumbnail.png" const JOURNAL_FILE := "journal.log" const DEFAULT_AUTOSAVE_GENERATIONS := 3 const MAX_AUTOSAVE_GENERATIONS := 16 const AUTOSAVE_SUFFIX := ".autosave." const MIGRATION_BACKUP_SUFFIX := ".pre_migration.bak" const SAVE_TRANSACTION_SUFFIX := ".save_transaction" const SAVE_TRANSACTION_MARKER := "THE_TREE_FORMAL_SAVE_V1\n" # Technical resource limits, independent of gameplay/content values. Check # authoritative file lengths and advertised expansion before allocating reads. const MAX_WORLD_BYTES := 256 * 1024 * 1024 const MAX_MANIFEST_BYTES := 4 * 1024 * 1024 static var _performance_budget := SimulationPerformanceBudget.new() static func save_snapshot(save_path: String, snapshot: Dictionary, metadata := {}) -> Dictionary: var started_at_us := Time.get_ticks_usec() var resolved := _resolve_safe_save_path(save_path) if not resolved[&"ok"]: return _complete_timed_operation(resolved, &"save", started_at_us) var result := _save_snapshot_absolute( resolved[&"path"], save_path, snapshot, metadata, FORMAT_VERSION, resolved[&"path"], resolved[&"path"] ) return _complete_timed_operation(result, &"save", started_at_us) static func load_snapshot(save_path: String) -> Dictionary: var started_at_us := Time.get_ticks_usec() var resolved := _resolve_safe_save_path(save_path) if not resolved[&"ok"]: return _complete_timed_operation(resolved, &"load", started_at_us) var reconcile := _reconcile_interrupted_formal_save(resolved[&"path"]) if not reconcile[&"ok"]: return _complete_timed_operation(reconcile, &"load", started_at_us) var result := _load_snapshot_absolute(resolved[&"path"], FORMAT_VERSION) result[&"interrupted_save_reconciled"] = bool(reconcile.get(&"reconciled", false)) return _complete_timed_operation(result, &"load", started_at_us) ## Reads only the named formal package. Unlike load_snapshot, this never ## reconciles, promotes, removes or migrates interrupted-save artifacts. A ## missing formal package stays missing even when a .tmp/.bak could recover it. static func inspect_snapshot(save_path: String) -> Dictionary: var started_at_us := Time.get_ticks_usec() var resolved := _resolve_safe_save_path(save_path) var result: Dictionary = resolved if resolved[&"ok"]: result = _load_snapshot_absolute(resolved[&"path"], FORMAT_VERSION) result[&"read_only"] = true return _complete_timed_operation(result, &"load", started_at_us) static func get_performance_report(metric_id: StringName) -> Dictionary: return _performance_budget.get_report(metric_id) static func get_performance_reports() -> Array[Dictionary]: return _performance_budget.get_all_reports() static func clear_performance_reports() -> void: _performance_budget.clear() ## Writes an autosave without touching the formal save or its .bak package. ## Generation zero is newest. Existing generations are moved transactionally; ## if candidate creation or rotation fails, the previously formal save remains ## byte-for-byte untouched. static func save_autosave( save_path: String, snapshot: Dictionary, metadata := {}, generation_count := DEFAULT_AUTOSAVE_GENERATIONS ) -> Dictionary: var started_at_us := Time.get_ticks_usec() if generation_count < 1 or generation_count > MAX_AUTOSAVE_GENERATIONS: return _complete_timed_operation( _failure(&"INVALID_GENERATION_COUNT", str(generation_count)), &"save", started_at_us ) var resolved := _resolve_safe_save_path(save_path) if not resolved[&"ok"]: return _complete_timed_operation(resolved, &"save", started_at_us) var target_path: String = resolved[&"path"] var pending_path := target_path + ".autosave.pending" var reconcile := _reconcile_autosave_rotation(target_path, pending_path, generation_count) if not reconcile[&"ok"]: return _complete_timed_operation(reconcile, &"save", started_at_us) var owned_paths: Array[String] = [pending_path, pending_path + ".tmp", pending_path + ".bak"] for generation in range(MAX_AUTOSAVE_GENERATIONS): owned_paths.append(target_path + AUTOSAVE_SUFFIX + str(generation)) owned_paths.append(target_path + AUTOSAVE_SUFFIX + str(generation) + ".rotate_tmp") if not _operation_paths_are_safe(owned_paths): return _complete_timed_operation( _failure(&"UNSAFE_PATH", "autosave path contains a symbolic link"), &"save", started_at_us ) var prune_preflight := _preflight_autosave_prune(target_path, generation_count) if not prune_preflight[&"ok"]: return _complete_timed_operation( _with_context(prune_preflight, {&"formal_save_preserved": true}), &"save", started_at_us ) for artifact_path: String in [pending_path, pending_path + ".tmp", pending_path + ".bak"]: if not _path_is_missing_or_package(artifact_path): return _complete_timed_operation( _failure(&"PACKAGE_PATH_CONFLICT", artifact_path), &"save", started_at_us ) _remove_tree(pending_path) _remove_tree(pending_path + ".tmp") _remove_tree(pending_path + ".bak") var candidate_metadata: Dictionary = metadata.duplicate(true) if metadata is Dictionary else {} candidate_metadata[&"save_kind"] = &"autosave" candidate_metadata[&"autosave_generation"] = 0 var candidate := _save_snapshot_absolute( pending_path, get_autosave_path(save_path, 0), snapshot, candidate_metadata, FORMAT_VERSION, target_path + AUTOSAVE_SUFFIX + "0", target_path, false ) if not candidate[&"ok"]: _remove_tree(pending_path) _remove_tree(pending_path + ".bak") return _complete_timed_operation( _with_context(candidate, {&"formal_save_preserved": true}), &"save", started_at_us ) var rotation := _rotate_autosaves(target_path, pending_path, generation_count) _remove_tree(pending_path + ".bak") if not rotation[&"ok"]: return _complete_timed_operation( _with_context(rotation, {&"formal_save_preserved": true}), &"save", started_at_us ) var pruned_generations: Array[int] = [] for generation_value: Variant in prune_preflight[&"generations"]: var generation := int(generation_value) _remove_tree(target_path + AUTOSAVE_SUFFIX + str(generation)) pruned_generations.append(generation) var result := { &"ok": true, &"code": &"OK", &"path": save_path, &"autosave_path": get_autosave_path(save_path, 0), &"generation": 0, &"generation_count": generation_count, &"manifest": candidate[&"manifest"], &"formal_save_preserved": true, &"interrupted_rotation_reconciled": bool(reconcile.get(&"reconciled", false)), &"pruned_generations": pruned_generations, } return _complete_timed_operation(result, &"save", started_at_us) static func get_autosave_path(save_path: String, generation: int) -> String: if generation < 0: return "" return save_path + AUTOSAVE_SUFFIX + str(generation) ## Returns checksum/schema status for every recovery candidate. Before reading, ## it completes or rolls back interrupted package renames using only verified ## packages. Selection priority is explicit and stable. static func inspect_recovery_candidates( save_path: String, generation_count := DEFAULT_AUTOSAVE_GENERATIONS ) -> Dictionary: if generation_count < 1 or generation_count > MAX_AUTOSAVE_GENERATIONS: return _failure(&"INVALID_GENERATION_COUNT", str(generation_count)) var resolved := _resolve_safe_save_path(save_path) if not resolved[&"ok"]: return resolved var target_path: String = resolved[&"path"] var formal_reconcile := _reconcile_interrupted_formal_save(target_path) if not formal_reconcile[&"ok"]: return formal_reconcile var migration_reconcile := _reconcile_interrupted_migration(target_path) if not migration_reconcile[&"ok"]: return migration_reconcile var autosave_reconcile := _reconcile_autosave_rotation( target_path, target_path + ".autosave.pending", generation_count ) if not autosave_reconcile[&"ok"]: return autosave_reconcile var descriptors: Array[Dictionary] = [ {&"id": &"formal", &"kind": &"formal", &"generation": -1, &"path": target_path}, {&"id": &"backup", &"kind": &"backup", &"generation": -1, &"path": target_path + ".bak"}, {&"id": &"migration_original", &"kind": &"migration_original", &"generation": -1, &"path": target_path + ".migration.original"}, {&"id": &"pre_migration_backup", &"kind": &"migration_backup", &"generation": -1, &"path": target_path + MIGRATION_BACKUP_SUFFIX}, ] for generation in range(generation_count): descriptors.append({ &"id": StringName("autosave_%d" % generation), &"kind": &"autosave", &"generation": generation, &"path": target_path + AUTOSAVE_SUFFIX + str(generation), }) var candidates: Array[Dictionary] = [] var selected: Dictionary = {} var selected_snapshot: Dictionary = {} for priority in range(descriptors.size()): var descriptor: Dictionary = descriptors[priority] var loaded := _load_snapshot_absolute(descriptor[&"path"], -1, false) var package_valid := bool(loaded[&"ok"]) var current_schema_valid := false var current_format := false if package_valid: current_format = int(loaded[&"manifest"].get("format_version", -1)) == FORMAT_VERSION current_schema_valid = bool(SimulationSnapshot.validate(loaded[&"snapshot"])[&"ok"]) var loadable := package_valid and current_format and current_schema_valid var candidate_code: StringName = StringName(loaded[&"code"]) if package_valid and not current_format: candidate_code = &"UNSUPPORTED_FORMAT" elif package_valid and not current_schema_valid: candidate_code = &"INVALID_SNAPSHOT" var candidate_info := { &"id": descriptor[&"id"], &"kind": descriptor[&"kind"], &"generation": descriptor[&"generation"], &"priority": priority, &"path": descriptor[&"path"], &"valid": loadable, &"package_valid": package_valid, &"requires_migration": package_valid and not loadable, &"code": candidate_code, &"message": str(loaded.get(&"message", "")), } if package_valid: candidate_info[&"manifest"] = loaded[&"manifest"].duplicate(true) if loadable and selected.is_empty(): selected = candidate_info.duplicate(true) selected_snapshot = loaded[&"snapshot"].duplicate(true) candidates.append(candidate_info) if selected.is_empty(): return { &"ok": false, &"code": &"NO_VALID_RECOVERY_CANDIDATE", &"message": save_path, &"selection_rule": &"FORMAL_BACKUP_MIGRATION_AUTOSAVE_NEWEST_FIRST", &"candidates": candidates, &"reconciliation": { &"formal_save": formal_reconcile, &"migration": migration_reconcile, &"autosave": autosave_reconcile, }, } return { &"ok": true, &"code": &"OK", &"selection_rule": &"FORMAL_BACKUP_MIGRATION_AUTOSAVE_NEWEST_FIRST", &"selected": selected, &"snapshot": selected_snapshot, &"manifest": selected[&"manifest"].duplicate(true), &"candidates": candidates, &"reconciliation": { &"formal_save": formal_reconcile, &"migration": migration_reconcile, &"autosave": autosave_reconcile, }, &"recovery_report": create_recovery_report(selected_snapshot, { &"candidate_id": selected[&"id"], &"candidate_path": selected[&"path"], }), } static func list_recovery_candidates( save_path: String, generation_count := DEFAULT_AUTOSAVE_GENERATIONS ) -> Dictionary: return inspect_recovery_candidates(save_path, generation_count) static func load_with_recovery( save_path: String, generation_count := DEFAULT_AUTOSAVE_GENERATIONS ) -> Dictionary: var started_at_us := Time.get_ticks_usec() var result := inspect_recovery_candidates(save_path, generation_count) return _complete_timed_operation(result, &"load", started_at_us) ## Loads one explicitly named recovery package. This is read-only: it never ## promotes the selected package and never repairs the snapshot. static func load_recovery_candidate( save_path: String, candidate_id: StringName, generation := -1 ) -> Dictionary: var started_at_us := Time.get_ticks_usec() var result := _load_recovery_candidate_impl(save_path, candidate_id, generation) return _complete_timed_operation(result, &"load", started_at_us) static func _load_recovery_candidate_impl( save_path: String, candidate_id: StringName, generation: int ) -> Dictionary: var resolved := _resolve_safe_save_path(save_path) if not resolved[&"ok"]: return resolved var target_path: String = resolved[&"path"] # Explicit candidate selection is a read-only operation. Normal load and # load_with_recovery retain transaction reconciliation at their own entry. var formal_guard := _guard_identified_formal_format(target_path) if not formal_guard[&"ok"]: return formal_guard var candidate_path := "" match candidate_id: &"formal": candidate_path = target_path &"backup": candidate_path = target_path + ".bak" &"migration_original": candidate_path = target_path + ".migration.original" &"pre_migration_backup": candidate_path = target_path + MIGRATION_BACKUP_SUFFIX &"autosave": if generation < 0 or generation >= MAX_AUTOSAVE_GENERATIONS: return _failure(&"INVALID_AUTOSAVE_GENERATION", str(generation)) candidate_path = target_path + AUTOSAVE_SUFFIX + str(generation) _: var candidate_text := str(candidate_id) if candidate_text.begins_with("autosave_") and candidate_text.trim_prefix("autosave_").is_valid_int(): generation = candidate_text.trim_prefix("autosave_").to_int() if generation < 0 or generation >= MAX_AUTOSAVE_GENERATIONS: return _failure(&"INVALID_AUTOSAVE_GENERATION", str(generation)) candidate_path = target_path + AUTOSAVE_SUFFIX + str(generation) else: return _failure(&"UNKNOWN_RECOVERY_CANDIDATE", str(candidate_id)) var result := _load_snapshot_absolute(candidate_path, FORMAT_VERSION) if not result[&"ok"]: return result result[&"candidate_id"] = candidate_id result[&"candidate_path"] = candidate_path result[&"read_only"] = true result[&"recovery_report"] = create_recovery_report(result[&"snapshot"], { &"candidate_id": candidate_id, &"candidate_path": candidate_path, }) return result ## Performs a continuous, explicit format migration and preserves an exact ## pre-migration package at .pre_migration.bak before promotion. The ## formal package is replaced only after both the migrated candidate and backup ## have passed checksum verification. static func migrate_save( save_path: String, registry: SaveMigrationRegistry, target_version := FORMAT_VERSION ) -> Dictionary: var started_at_us := Time.get_ticks_usec() var result := _migrate_save_impl(save_path, registry, target_version) return _complete_timed_operation(result, &"save", started_at_us) ## Domain migrations keep the package format version but still require the ## same checksum-verified backup and atomic promotion as format migrations. ## The caller validates domain invariants before supplying the candidate. static func replace_snapshot_with_backup(save_path: String, expected: Dictionary, candidate: Dictionary) -> Dictionary: if expected.is_empty() or candidate.is_empty(): return _failure(&"MIGRATION_SNAPSHOT_REQUIRED", save_path) var validation := SimulationSnapshot.validate(candidate) if not validation[&"ok"]: return _failure(&"MIGRATION_INVALID_SNAPSHOT", ", ".join(validation[&"errors"])) var started := Time.get_ticks_usec() return _complete_timed_operation(_migrate_save_impl(save_path, null, FORMAT_VERSION, expected, candidate), &"save", started) static func _migrate_save_impl( save_path: String, registry: SaveMigrationRegistry, target_version: int, expected_snapshot: Dictionary = {}, replacement_snapshot: Dictionary = {} ) -> Dictionary: if registry == null and replacement_snapshot.is_empty(): return _failure(&"MIGRATION_REGISTRY_REQUIRED", save_path) var resolved := _resolve_safe_save_path(save_path) if not resolved[&"ok"]: return resolved var target_path: String = resolved[&"path"] var formal_guard := _guard_identified_formal_format(target_path) var migrating_registered_legacy := false if not formal_guard[&"ok"]: if formal_guard[&"code"] != &"UNSUPPORTED_FORMAT": return formal_guard var identified_version := int(formal_guard[&"identified_format_version"]) if identified_version > target_version: return _failure(&"MIGRATION_DOWNGRADE_UNSUPPORTED", "%d -> %d" % [identified_version, target_version]) if registry == null or not replacement_snapshot.is_empty(): return formal_guard if target_version != FORMAT_VERSION or identified_version >= target_version \ or formal_guard[&"identified_format_versions"].size() != 1: return formal_guard for version in range(identified_version, target_version): if not registry.has_step(version): return _failure(&"MIGRATION_STEP_MISSING", "missing adjacent migration %d -> %d" % [version, version + 1]) # A registered old formal source is decoded/migrated directly. Do not # reconcile stale normal/migration artifacts before source validation. migrating_registered_legacy = true if not migrating_registered_legacy: var formal_reconcile := _reconcile_interrupted_formal_save(target_path) if not formal_reconcile[&"ok"]: return formal_reconcile var reconcile := _reconcile_interrupted_migration(target_path) if not reconcile[&"ok"]: return reconcile var operation_paths: Array[String] = [ target_path, target_path + ".migration.tmp", target_path + ".migration.original", target_path + MIGRATION_BACKUP_SUFFIX, target_path + MIGRATION_BACKUP_SUFFIX + ".tmp", target_path + MIGRATION_BACKUP_SUFFIX + ".old", ] if not _operation_paths_are_safe(operation_paths): return _failure(&"UNSAFE_PATH", "migration path contains a symbolic link") for artifact_path: String in [ target_path + ".migration.tmp", target_path + ".migration.original", target_path + MIGRATION_BACKUP_SUFFIX + ".tmp", target_path + MIGRATION_BACKUP_SUFFIX + ".old", ]: if not _path_is_missing_or_package(artifact_path): return _failure(&"PACKAGE_PATH_CONFLICT", artifact_path) # Old format snapshots are intentionally decoded and checksum-verified before # migration, but validated only after the final adjacent migration step. var source := _load_snapshot_absolute(target_path, -1, false) if not source[&"ok"]: return _with_context(source, {&"migration_started": false}) var from_version := int(source[&"manifest"].get("format_version", -1)) if target_version < from_version: return _failure(&"MIGRATION_DOWNGRADE_UNSUPPORTED", "%d -> %d" % [from_version, target_version]) if not replacement_snapshot.is_empty(): if from_version != target_version: return _failure(&"FORMAT_MIGRATION_REQUIRED", save_path) if source[&"snapshot"] == replacement_snapshot: return {&"ok": true, &"code": &"ALREADY_CURRENT", &"backup_created": false, &"path": save_path} if source[&"snapshot"] != expected_snapshot: return _failure(&"MIGRATION_SOURCE_CHANGED", save_path) if target_version == from_version and replacement_snapshot.is_empty(): var current_validation := SimulationSnapshot.validate(source[&"snapshot"]) if target_version == FORMAT_VERSION and not current_validation[&"ok"]: return _failure(&"INVALID_SNAPSHOT", ", ".join(current_validation[&"errors"])) return { &"ok": true, &"code": &"ALREADY_CURRENT", &"path": save_path, &"from_version": from_version, &"target_version": target_version, &"applied_migration_ids": [], &"backup_created": false, } var payload := { &"manifest": source[&"manifest"].duplicate(true), &"snapshot": source[&"snapshot"].duplicate(true), } var migrated: Dictionary if replacement_snapshot.is_empty(): migrated = registry.migrate(payload, from_version, target_version) else: payload[&"snapshot"] = replacement_snapshot.duplicate(true) migrated = {&"ok": true, &"data": payload, &"applied_migration_ids": [&"domain_snapshot_replacement"]} if not migrated[&"ok"]: return _with_context(migrated, { &"path": save_path, &"from_version": from_version, &"target_version": target_version, &"migration_started": false, }) var migrated_payload: Dictionary = migrated[&"data"] if not migrated_payload.get(&"manifest", null) is Dictionary: return _failure(&"MIGRATION_INVALID_MANIFEST", save_path) if not migrated_payload.get(&"snapshot", null) is Dictionary: return _failure(&"MIGRATION_INVALID_SNAPSHOT", save_path) var migrated_snapshot: Dictionary = migrated_payload[&"snapshot"] var validation := SimulationSnapshot.validate(migrated_snapshot) if not validation[&"ok"]: return _failure(&"MIGRATION_INVALID_SNAPSHOT", ", ".join(validation[&"errors"])) var migrated_manifest: Dictionary = migrated_payload[&"manifest"].duplicate(true) migrated_manifest[&"format_version"] = target_version var candidate_path := target_path + ".migration.tmp" var backup_path := target_path + MIGRATION_BACKUP_SUFFIX var backup_temp_path := backup_path + ".tmp" var backup_old_path := backup_path + ".old" var original_hold_path := target_path + ".migration.original" _remove_tree(candidate_path) _remove_tree(backup_temp_path) var candidate_write := _write_package_directory( candidate_path, migrated_snapshot, migrated_manifest, target_version, target_path ) if not candidate_write[&"ok"]: _remove_tree(candidate_path) return _with_context(candidate_write, {&"migration_started": false}) var candidate_verify := _load_snapshot_absolute(candidate_path, target_version) if not candidate_verify[&"ok"]: _remove_tree(candidate_path) return _failure(&"MIGRATION_VERIFY_FAILED", str(candidate_verify.get(&"message", candidate_verify[&"code"]))) var copy_error := _copy_tree(target_path, backup_temp_path) if copy_error != OK: _remove_tree(candidate_path) _remove_tree(backup_temp_path) return _failure(&"MIGRATION_BACKUP_COPY_FAILED", error_string(copy_error)) var backup_verify := _load_snapshot_absolute(backup_temp_path, from_version, false) if not backup_verify[&"ok"]: _remove_tree(candidate_path) _remove_tree(backup_temp_path) return _failure(&"MIGRATION_BACKUP_VERIFY_FAILED", str(backup_verify.get(&"message", backup_verify[&"code"]))) if DirAccess.dir_exists_absolute(backup_path): var old_backup_error := DirAccess.rename_absolute(backup_path, backup_old_path) if old_backup_error != OK: _remove_tree(candidate_path) _remove_tree(backup_temp_path) return _failure(&"MIGRATION_BACKUP_ROTATE_FAILED", error_string(old_backup_error)) var backup_promote_error := DirAccess.rename_absolute(backup_temp_path, backup_path) if backup_promote_error != OK: if DirAccess.dir_exists_absolute(backup_old_path): DirAccess.rename_absolute(backup_old_path, backup_path) _remove_tree(candidate_path) _remove_tree(backup_temp_path) return _failure(&"MIGRATION_BACKUP_PROMOTE_FAILED", error_string(backup_promote_error)) var hold_error := DirAccess.rename_absolute(target_path, original_hold_path) if hold_error != OK: _restore_previous_migration_backup(backup_path, backup_old_path) _remove_tree(candidate_path) return _failure(&"MIGRATION_HOLD_FAILED", error_string(hold_error)) var promote_error := DirAccess.rename_absolute(candidate_path, target_path) if promote_error != OK: DirAccess.rename_absolute(original_hold_path, target_path) _restore_previous_migration_backup(backup_path, backup_old_path) _remove_tree(candidate_path) return _failure(&"MIGRATION_PROMOTE_FAILED", error_string(promote_error)) var promoted_verify := _load_snapshot_absolute(target_path, target_version) if not promoted_verify[&"ok"]: _remove_tree(target_path) DirAccess.rename_absolute(original_hold_path, target_path) _restore_previous_migration_backup(backup_path, backup_old_path) return _failure( &"MIGRATION_PROMOTED_VERIFY_FAILED", str(promoted_verify.get(&"message", promoted_verify[&"code"])) ) _remove_tree(original_hold_path) _remove_tree(backup_old_path) return { &"ok": true, &"code": &"OK", &"path": save_path, &"backup_path": save_path + MIGRATION_BACKUP_SUFFIX, &"from_version": from_version, &"target_version": target_version, &"applied_migration_ids": migrated[&"applied_migration_ids"].duplicate(), &"backup_created": true, &"manifest": candidate_verify[&"manifest"].duplicate(true), } ## Boundary for post-load diagnostics. It intentionally does not inspect or ## mutate domain ownership such as Task reservations or Event actor locks; those ## authoritative services can append findings and decide repairs transactionally. static func create_recovery_report(snapshot: Dictionary, source := {}) -> Dictionary: var validation := SimulationSnapshot.validate(snapshot) return { &"report_version": 1, &"snapshot_schema_valid": bool(validation[&"ok"]), &"snapshot_errors": validation[&"errors"].duplicate(), &"source": source.duplicate(true) if source is Dictionary else {}, &"automatic_repairs_applied": false, &"repair_policy": &"REPORT_ONLY_AUTHORITATIVE_SERVICE_REQUIRED", &"findings": { &"orphan_tasks": [], &"orphan_event_locks": [], &"orphan_inventory_reservations": [], }, } static func delete_save(save_path: String) -> bool: var resolved := _resolve_safe_save_path(save_path) if not resolved[&"ok"]: return false var target_path: String = resolved[&"path"] if DirAccess.dir_exists_absolute(target_path) and not _looks_like_save_package(target_path): # A save slot is a package directory, never an arbitrary parent directory. return false var owned_paths: Array[String] = [ target_path, target_path + ".tmp", target_path + ".bak", target_path + ".autosave.pending", target_path + ".autosave.pending.tmp", target_path + ".autosave.pending.bak", target_path + MIGRATION_BACKUP_SUFFIX, target_path + MIGRATION_BACKUP_SUFFIX + ".tmp", target_path + MIGRATION_BACKUP_SUFFIX + ".old", target_path + ".migration.tmp", target_path + ".migration.original", target_path + SAVE_TRANSACTION_SUFFIX, ] for generation in range(MAX_AUTOSAVE_GENERATIONS): owned_paths.append(target_path + AUTOSAVE_SUFFIX + str(generation)) owned_paths.append(target_path + AUTOSAVE_SUFFIX + str(generation) + ".rotate_tmp") if not _operation_paths_are_safe(owned_paths): return false var transaction_marker_path := target_path + SAVE_TRANSACTION_SUFFIX if (FileAccess.file_exists(transaction_marker_path) or DirAccess.dir_exists_absolute(transaction_marker_path)) \ and not _formal_transaction_marker_is_valid(transaction_marker_path): return false for path: String in owned_paths: if not FileAccess.file_exists(path) and not DirAccess.dir_exists_absolute(path): continue if path == target_path + SAVE_TRANSACTION_SUFFIX: if _formal_transaction_marker_is_valid(path): DirAccess.remove_absolute(path) elif _looks_like_save_package(path): _remove_tree(path) return true static func _save_snapshot_absolute( target_path: String, reported_path: String, snapshot: Dictionary, metadata: Variant, format_version: int, logical_target_path := "", identity_source_path := "", use_transaction_marker := true ) -> Dictionary: var validation := SimulationSnapshot.validate(snapshot) if not validation[&"ok"]: return _failure(&"INVALID_SNAPSHOT", ", ".join(validation[&"errors"])) var temporary_path := target_path + ".tmp" var backup_path := target_path + ".bak" var transaction_marker_path := target_path + SAVE_TRANSACTION_SUFFIX if not _operation_paths_are_safe([target_path, temporary_path, backup_path, transaction_marker_path]): return _failure(&"UNSAFE_PATH", "save path contains a symbolic link") var interrupted_reconcile := {&"ok": true, &"reconciled": false, &"action": &"none"} if use_transaction_marker: interrupted_reconcile = _reconcile_interrupted_formal_save(target_path) if not interrupted_reconcile[&"ok"]: return interrupted_reconcile if not _path_is_missing_or_package(target_path): return _failure(&"NOT_A_SAVE_PACKAGE", target_path) if not _path_is_missing_or_package(temporary_path): return _failure(&"PACKAGE_PATH_CONFLICT", temporary_path) if not _path_is_missing_or_package(backup_path): return _failure(&"BACKUP_PATH_CONFLICT", backup_path) var logical_path: String = logical_target_path if not logical_target_path.is_empty() else target_path var identity_path: String = identity_source_path if not identity_source_path.is_empty() else target_path var effective_metadata := _merge_save_metadata( metadata if metadata is Dictionary else {}, _read_manifest_identity(identity_path), logical_path ) if not effective_metadata.has(&"thumbnail_png"): var previous_thumbnail_path := identity_path.path_join(THUMBNAIL_FILE) if FileAccess.file_exists(previous_thumbnail_path): effective_metadata[&"_thumbnail_png"] = FileAccess.get_file_as_bytes(previous_thumbnail_path) _remove_tree(temporary_path) var write_result := _write_package_directory( temporary_path, snapshot, effective_metadata, format_version, logical_path ) if not write_result[&"ok"]: _remove_tree(temporary_path) return write_result var verification := _load_snapshot_absolute(temporary_path, format_version) if not verification[&"ok"]: _remove_tree(temporary_path) return _failure(&"VERIFY_FAILED", str(verification.get(&"message", "unknown"))) if use_transaction_marker: var marker_error := _write_formal_transaction_marker(transaction_marker_path) if marker_error != OK: _remove_tree(temporary_path) return _failure(&"TRANSACTION_MARKER_WRITE_FAILED", error_string(marker_error)) _remove_tree(backup_path) if DirAccess.dir_exists_absolute(target_path): var backup_error := DirAccess.rename_absolute(target_path, backup_path) if backup_error != OK: _remove_tree(temporary_path) _remove_formal_transaction_marker(transaction_marker_path) return _failure(&"BACKUP_FAILED", error_string(backup_error)) var promote_error := DirAccess.rename_absolute(temporary_path, target_path) if promote_error != OK: var rollback_verified := false if DirAccess.dir_exists_absolute(backup_path): var rollback_error := DirAccess.rename_absolute(backup_path, target_path) rollback_verified = rollback_error == OK and bool(_package_status(target_path)[&"current_loadable"]) _remove_tree(temporary_path) if rollback_verified: _remove_formal_transaction_marker(transaction_marker_path) return _failure(&"PROMOTE_FAILED", error_string(promote_error)) var promoted_status := _package_status(target_path) if not promoted_status[&"current_loadable"]: if _looks_like_save_package(target_path): _remove_tree(target_path) var rollback_verified := false if DirAccess.dir_exists_absolute(backup_path): var rollback_error := DirAccess.rename_absolute(backup_path, target_path) rollback_verified = rollback_error == OK and bool(_package_status(target_path)[&"current_loadable"]) if rollback_verified: _remove_formal_transaction_marker(transaction_marker_path) return _failure(&"PROMOTE_VERIFY_FAILED", str(promoted_status.get(&"code", &"INVALID_PACKAGE"))) if use_transaction_marker: _remove_formal_transaction_marker(transaction_marker_path) return { &"ok": true, &"code": &"OK", &"path": reported_path, &"manifest": verification[&"manifest"].duplicate(true), &"interrupted_save_reconciled": bool(interrupted_reconcile.get(&"reconciled", false)), } static func _write_package_directory( package_path: String, snapshot: Dictionary, metadata: Dictionary, format_version: int, logical_target_path := "" ) -> Dictionary: if not _operation_paths_are_safe([package_path]): return _failure(&"UNSAFE_PATH", "package path contains a symbolic link") if not _path_is_missing_or_package(package_path): return _failure(&"PACKAGE_PATH_CONFLICT", package_path) _remove_tree(package_path) var make_error := DirAccess.make_dir_recursive_absolute(package_path) if make_error != OK: return _failure(&"CREATE_DIRECTORY_FAILED", error_string(make_error)) var raw_world := var_to_bytes(snapshot) if not _world_size_is_supported(raw_world.size()): _remove_tree(package_path) return _failure(&"SAVE_SIZE_LIMIT_EXCEEDED", "uncompressed world exceeds technical resource limit") var compressed_world := raw_world.compress(FileAccess.COMPRESSION_ZSTD) if compressed_world.is_empty() and not raw_world.is_empty(): _remove_tree(package_path) return _failure(&"COMPRESSION_FAILED", WORLD_FILE) if not _world_size_is_supported(compressed_world.size()): _remove_tree(package_path) return _failure(&"SAVE_SIZE_LIMIT_EXCEEDED", "compressed world exceeds technical resource limit") var checksum := _sha256(compressed_world) var world_error := _write_world( package_path.path_join(WORLD_FILE), raw_world.size(), compressed_world, format_version ) if world_error != OK: _remove_tree(package_path) return _failure(&"WRITE_WORLD_FAILED", error_string(world_error)) var logical_name := ( logical_target_path.get_file() if not logical_target_path.is_empty() else package_path.get_file() ) var now := Time.get_datetime_string_from_system(true) var manifest := { &"format_version": format_version, &"game_version": str(ProjectSettings.get_setting("application/config/version", "0.1.0")), &"save_id": str(metadata.get(&"save_id", logical_name)), &"display_name": str(metadata.get(&"display_name", logical_name)), &"created_at_real": str(metadata.get(&"created_at_real", now)), &"updated_at_real": now, &"absolute_minute": int(snapshot[&"clock"][&"absolute_minute"]), &"day": int(snapshot[&"clock"].get(&"day", int(snapshot[&"clock"][&"absolute_minute"]) / 1_440 + 1)), &"seed": int(metadata.get(&"seed", _snapshot_seed(snapshot))), &"content_pack_ids": _metadata_array(metadata.get(&"content_pack_ids", [])), &"enabled_filter_profile": metadata.get(&"enabled_filter_profile", &"default"), &"checksum_world": checksum, &"uncompressed_size": raw_world.size(), } for key: Variant in metadata: if key in [&"thumbnail_png", &"thumbnail_path", &"_thumbnail_png"]: continue if not manifest.has(key): manifest[key] = metadata[key] var manifest_text := JSON.stringify(manifest, "\t") if manifest_text.to_utf8_buffer().size() > MAX_MANIFEST_BYTES: _remove_tree(package_path) return _failure(&"SAVE_SIZE_LIMIT_EXCEEDED", "manifest exceeds technical resource limit") var manifest_error := _write_text(package_path.path_join(MANIFEST_FILE), manifest_text) if manifest_error != OK: _remove_tree(package_path) return _failure(&"WRITE_MANIFEST_FAILED", error_string(manifest_error)) var thumbnail_error := _ensure_thumbnail(package_path, metadata) if thumbnail_error != OK: _remove_tree(package_path) return _failure(&"WRITE_THUMBNAIL_FAILED", error_string(thumbnail_error)) var journal_error := _write_text( package_path.path_join(JOURNAL_FILE), "save_completed %s\n" % manifest[&"updated_at_real"] ) if journal_error != OK: _remove_tree(package_path) return _failure(&"WRITE_JOURNAL_FAILED", error_string(journal_error)) return {&"ok": true, &"code": &"OK", &"manifest": manifest} static func _load_snapshot_absolute( target_path: String, expected_format_version: int, validate_snapshot := true ) -> Dictionary: var read_boundary := _validate_package_read_boundary(target_path) if not read_boundary[&"ok"]: return read_boundary var manifest_path := target_path.path_join(MANIFEST_FILE) var world_path := target_path.path_join(WORLD_FILE) if not FileAccess.file_exists(manifest_path) or not FileAccess.file_exists(world_path): return _failure(&"SAVE_NOT_FOUND", target_path) # Recheck the exact entry immediately before each open. The recursive package # scan above protects all known package entries; these checks narrow the # remaining TOCTOU window at the two authoritative reads. if not _package_file_is_safe_for_read(manifest_path): return _failure(&"UNSAFE_PATH", "symbolic link at package read boundary: %s" % manifest_path) var manifest_file := FileAccess.open(manifest_path, FileAccess.READ) if manifest_file == null: return _failure(&"INVALID_MANIFEST", error_string(FileAccess.get_open_error())) if manifest_file.get_length() > MAX_MANIFEST_BYTES: return _failure(&"SAVE_SIZE_LIMIT_EXCEEDED", "manifest exceeds technical resource limit") var manifest_text := manifest_file.get_as_text() manifest_file = null var parser := JSON.new() if parser.parse(manifest_text) != OK: return _failure(&"INVALID_MANIFEST", manifest_path) var manifest_value: Variant = parser.data if not manifest_value is Dictionary: return _failure(&"INVALID_MANIFEST", manifest_path) var manifest: Dictionary = manifest_value var version_value: Variant = manifest.get("format_version", -1) # Godot JSON decodes numbers as floats. Permit exact integral JSON numbers, # while rejecting strings, booleans and fractions instead of coercing them. if not _is_integral_json_number(version_value, 0, 0xffffffff): return _failure(&"INVALID_MANIFEST", "format_version must be an unsigned integer") var manifest_version := int(version_value) if expected_format_version >= 0 and manifest_version != expected_format_version: return _failure(&"UNSUPPORTED_FORMAT", str(manifest_version)) if not _package_file_is_safe_for_read(world_path): return _failure(&"UNSAFE_PATH", "symbolic link at package read boundary: %s" % world_path) var world_file := FileAccess.open(world_path, FileAccess.READ) if world_file == null: return _failure(&"READ_WORLD_FAILED", error_string(FileAccess.get_open_error())) if world_file.get_length() < 16: return _failure(&"INVALID_WORLD_HEADER", world_path) if world_file.get_length() - 16 > MAX_WORLD_BYTES: return _failure(&"SAVE_SIZE_LIMIT_EXCEEDED", "compressed world exceeds technical resource limit") var magic := world_file.get_buffer(4).get_string_from_ascii() var stored_version := world_file.get_32() var uncompressed_size := world_file.get_64() if not _world_size_is_supported(uncompressed_size): return _failure(&"SAVE_SIZE_LIMIT_EXCEEDED", "uncompressed world exceeds technical resource limit") var compressed_size := world_file.get_length() - world_file.get_position() if compressed_size < 0: return _failure(&"INVALID_WORLD_HEADER", world_path) var compressed_world := world_file.get_buffer(compressed_size) if magic != MAGIC or stored_version != manifest_version: return _failure(&"INVALID_WORLD_HEADER", world_path) var checksum_value: Variant = manifest.get("checksum_world", null) if not checksum_value is String or checksum_value.length() != 64 or not checksum_value.is_valid_hex_number(false): return _failure(&"INVALID_MANIFEST", "checksum_world must be a SHA-256 string") var expected_checksum: String = checksum_value if _sha256(compressed_world) != expected_checksum: return _failure(&"CHECKSUM_MISMATCH", world_path) # Older packages may omit this duplicate size field; the binary header still # supplies a required bounded size. If present it must be exact and agree. if manifest.has("uncompressed_size"): var size_value: Variant = manifest["uncompressed_size"] if not _is_integral_json_number(size_value, 1, MAX_WORLD_BYTES): return _failure(&"INVALID_MANIFEST", "uncompressed_size must be a supported positive integer") if int(size_value) != uncompressed_size: return _failure(&"SIZE_MISMATCH", world_path) var raw_world := compressed_world.decompress(uncompressed_size, FileAccess.COMPRESSION_ZSTD) if raw_world.size() != uncompressed_size: return _failure(&"DECOMPRESSION_FAILED", world_path) var snapshot_value: Variant = bytes_to_var(raw_world) if not snapshot_value is Dictionary: return _failure(&"INVALID_WORLD_DATA", world_path) var snapshot: Dictionary = snapshot_value if validate_snapshot: var validation := SimulationSnapshot.validate(snapshot) if not validation[&"ok"]: return _failure(&"INVALID_SNAPSHOT", ", ".join(validation[&"errors"])) return { &"ok": true, &"code": &"OK", &"manifest": manifest, &"snapshot": snapshot, } static func _is_integral_json_number(value: Variant, minimum: int, maximum: int) -> bool: if typeof(value) == TYPE_INT: return value >= minimum and value <= maximum if typeof(value) != TYPE_FLOAT or not is_finite(value): return false return value >= minimum and value <= maximum and value == floor(value) static func _world_size_is_supported(size: int) -> bool: return size > 0 and size <= MAX_WORLD_BYTES ## Probe only bounded metadata and the fixed binary prefix. An identifiable ## unsupported formal format must be rejected before any transaction artifact ## is reconciled, even when its compressed payload/checksum is damaged. static func _guard_identified_formal_format(target_path: String) -> Dictionary: var versions: Array[int] = [] var manifest_path := target_path.path_join(MANIFEST_FILE) if FileAccess.file_exists(manifest_path): if not _package_file_is_safe_for_read(manifest_path): return _failure(&"UNSAFE_PATH", manifest_path) var manifest_file := FileAccess.open(manifest_path, FileAccess.READ) if manifest_file == null: return _failure(&"INVALID_MANIFEST", error_string(FileAccess.get_open_error())) if manifest_file.get_length() <= MAX_MANIFEST_BYTES: var parser := JSON.new() if parser.parse(manifest_file.get_as_text()) == OK and parser.data is Dictionary: var value: Variant = parser.data.get("format_version", null) if _is_integral_json_number(value, 0, 0xffffffff): versions.append(int(value)) manifest_file = null var world_path := target_path.path_join(WORLD_FILE) if FileAccess.file_exists(world_path): if not _package_file_is_safe_for_read(world_path): return _failure(&"UNSAFE_PATH", world_path) var world_file := FileAccess.open(world_path, FileAccess.READ) if world_file == null: return _failure(&"READ_WORLD_FAILED", error_string(FileAccess.get_open_error())) if world_file.get_length() >= 8 and world_file.get_buffer(4).get_string_from_ascii() == MAGIC: var version := world_file.get_32() if version not in versions: versions.append(version) world_file = null for version: int in versions: if version != FORMAT_VERSION: var failure := _failure(&"UNSUPPORTED_FORMAT", str(version)) failure[&"identified_format_version"] = version failure[&"identified_format_versions"] = versions failure[&"formal_and_artifacts_preserved"] = true return failure return {&"ok": true, &"code": &"OK"} ## Resolves a normal-save promotion interrupted after the old formal package ## moved to .bak. Only checksum/schema-verified packages may become formal. ## The marker is durable, but verified .tmp/.bak artifacts also recover saves ## created by older builds which could crash before this marker existed. static func _reconcile_interrupted_formal_save(target_path: String) -> Dictionary: var temporary_path := target_path + ".tmp" var backup_path := target_path + ".bak" var marker_path := target_path + SAVE_TRANSACTION_SUFFIX if not _operation_paths_are_safe([target_path, temporary_path, backup_path, marker_path]): return _failure(&"UNSAFE_PATH", "formal save recovery path contains a symbolic link") var formal_guard := _guard_identified_formal_format(target_path) if not formal_guard[&"ok"]: return formal_guard var marker_exists := FileAccess.file_exists(marker_path) or DirAccess.dir_exists_absolute(marker_path) if marker_exists and not _formal_transaction_marker_is_valid(marker_path): return _failure(&"TRANSACTION_MARKER_CONFLICT", marker_path) var target_status := _package_status(target_path) var temporary_status := _package_status(temporary_path) var backup_status := _package_status(backup_path) if target_status[&"current_loadable"]: if not marker_exists: return {&"ok": true, &"code": &"OK", &"reconciled": false, &"action": &"none"} if temporary_status[&"exists"]: if not _looks_like_save_package(temporary_path): return _failure(&"FORMAL_RECOVERY_TEMP_CONFLICT", temporary_path) _remove_tree(temporary_path) _remove_formal_transaction_marker(marker_path) return { &"ok": true, &"code": &"OK", &"reconciled": true, &"action": &"kept_verified_formal", } # Without a durable marker, an invalid visible formal package remains a # recovery candidate and may fall back read-only to .bak/autosaves. Never # destroy it merely because it failed verification. if target_status[&"exists"] and not marker_exists: return {&"ok": true, &"code": &"OK", &"reconciled": false, &"action": &"none"} if target_status[&"exists"]: if not _looks_like_save_package(target_path): return _failure(&"FORMAL_RECOVERY_TARGET_CONFLICT", target_path) var replacement_path := "" var replacement_action: StringName = &"" if temporary_status[&"current_loadable"]: replacement_path = temporary_path replacement_action = &"replaced_invalid_formal_with_candidate" elif backup_status[&"current_loadable"]: replacement_path = backup_path replacement_action = &"replaced_invalid_formal_with_backup" if replacement_path.is_empty(): return _failure(&"FORMAL_RECOVERY_NO_VALID_PACKAGE", target_path) _remove_tree(target_path) var replace_error := DirAccess.rename_absolute(replacement_path, target_path) if replace_error != OK: return _failure(&"FORMAL_RECOVERY_PROMOTE_FAILED", error_string(replace_error)) if not bool(_package_status(target_path)[&"current_loadable"]): return _failure(&"FORMAL_RECOVERY_VERIFY_FAILED", target_path) if replacement_path != temporary_path and temporary_status[&"exists"] \ and _looks_like_save_package(temporary_path): _remove_tree(temporary_path) _remove_formal_transaction_marker(marker_path) return { &"ok": true, &"code": &"OK", &"reconciled": true, &"action": replacement_action, } var source_path := "" var action: StringName = &"" if temporary_status[&"current_loadable"]: source_path = temporary_path action = &"promoted_verified_candidate" elif backup_status[&"current_loadable"]: source_path = backup_path action = &"restored_verified_backup" if source_path.is_empty(): if marker_exists: return _failure(&"FORMAL_RECOVERY_NO_VALID_PACKAGE", target_path) return {&"ok": true, &"code": &"OK", &"reconciled": false, &"action": &"none"} var promote_error := DirAccess.rename_absolute(source_path, target_path) if promote_error != OK: return _failure(&"FORMAL_RECOVERY_PROMOTE_FAILED", error_string(promote_error)) if not bool(_package_status(target_path)[&"current_loadable"]): return _failure(&"FORMAL_RECOVERY_VERIFY_FAILED", target_path) if source_path != temporary_path and marker_exists and temporary_status[&"exists"] \ and _looks_like_save_package(temporary_path): _remove_tree(temporary_path) _remove_formal_transaction_marker(marker_path) return { &"ok": true, &"code": &"OK", &"reconciled": true, &"action": action, } static func _rotate_autosaves(target_path: String, pending_path: String, generation_count: int) -> Dictionary: var reconciliation := _reconcile_autosave_rotation(target_path, pending_path, generation_count) if not reconciliation[&"ok"]: _remove_tree(pending_path) return reconciliation var moved_to_transaction: Array[int] = [] for generation in range(generation_count): var generation_path := target_path + AUTOSAVE_SUFFIX + str(generation) if not DirAccess.dir_exists_absolute(generation_path): continue var transaction_path := generation_path + ".rotate_tmp" var hold_error := DirAccess.rename_absolute(generation_path, transaction_path) if hold_error != OK: _rollback_autosave_holds(target_path, moved_to_transaction) _remove_tree(pending_path) return _failure(&"AUTOSAVE_ROTATION_HOLD_FAILED", error_string(hold_error)) moved_to_transaction.append(generation) var newest_path := target_path + AUTOSAVE_SUFFIX + "0" var promote_error := DirAccess.rename_absolute(pending_path, newest_path) if promote_error != OK: _rollback_autosave_holds(target_path, moved_to_transaction) _remove_tree(pending_path) return _failure(&"AUTOSAVE_PROMOTE_FAILED", error_string(promote_error)) var shifted: Array[int] = [] for generation in moved_to_transaction: if generation >= generation_count - 1: continue var transaction_path := target_path + AUTOSAVE_SUFFIX + str(generation) + ".rotate_tmp" var destination_path := target_path + AUTOSAVE_SUFFIX + str(generation + 1) var shift_error := DirAccess.rename_absolute(transaction_path, destination_path) if shift_error != OK: _remove_tree(newest_path) for shifted_generation in shifted: var shifted_path := target_path + AUTOSAVE_SUFFIX + str(shifted_generation + 1) var original_hold := target_path + AUTOSAVE_SUFFIX + str(shifted_generation) + ".rotate_tmp" DirAccess.rename_absolute(shifted_path, original_hold) _rollback_autosave_holds(target_path, moved_to_transaction) return _failure(&"AUTOSAVE_ROTATION_SHIFT_FAILED", error_string(shift_error)) shifted.append(generation) for generation in moved_to_transaction: _remove_tree(target_path + AUTOSAVE_SUFFIX + str(generation) + ".rotate_tmp") return {&"ok": true, &"code": &"OK"} ## Retention shrink is allowed to delete only checksum-valid packages which ## occupy generation names owned by this slot. Preflight happens before the new ## autosave is committed so a foreign directory or link cannot cause a partial ## rotation followed by a prune failure. static func _preflight_autosave_prune(target_path: String, generation_count: int) -> Dictionary: var generations: Array[int] = [] for generation in range(generation_count, MAX_AUTOSAVE_GENERATIONS): var generation_path := target_path + AUTOSAVE_SUFFIX + str(generation) if not FileAccess.file_exists(generation_path) and not DirAccess.dir_exists_absolute(generation_path): continue var status := _package_status(generation_path) if not status[&"package_valid"]: return _failure(&"AUTOSAVE_PRUNE_CONFLICT", generation_path) generations.append(generation) return {&"ok": true, &"code": &"OK", &"generations": generations} ## Repairs a rotation interrupted at any rename boundary. If the pending ## candidate still exists, the old generations are rolled back. Once generation ## zero has been promoted, the remaining shifts are completed instead. static func _reconcile_autosave_rotation( target_path: String, pending_path: String, generation_count: int ) -> Dictionary: var paths: Array[String] = [pending_path] var held_generations: Array[int] = [] # Scan the full supported range because the configured retention count may # have changed between the interrupted run and recovery. for generation in range(MAX_AUTOSAVE_GENERATIONS): var generation_path := target_path + AUTOSAVE_SUFFIX + str(generation) var hold_path := generation_path + ".rotate_tmp" paths.append(generation_path) paths.append(hold_path) if DirAccess.dir_exists_absolute(hold_path): held_generations.append(generation) if not _operation_paths_are_safe(paths): return _failure(&"UNSAFE_PATH", "autosave recovery path contains a symbolic link") if held_generations.is_empty(): return {&"ok": true, &"code": &"OK", &"reconciled": false, &"action": &"none"} var pending_exists := DirAccess.dir_exists_absolute(pending_path) var newest_exists := DirAccess.dir_exists_absolute(target_path + AUTOSAVE_SUFFIX + "0") if pending_exists or not newest_exists: # A hold and its original slot may never coexist in a valid rollback state. for generation in held_generations: if DirAccess.dir_exists_absolute(target_path + AUTOSAVE_SUFFIX + str(generation)): return _failure( &"AUTOSAVE_ROTATION_AMBIGUOUS", "generation %d has both live and held packages" % generation ) _rollback_autosave_holds(target_path, held_generations) for generation in held_generations: if DirAccess.dir_exists_absolute(target_path + AUTOSAVE_SUFFIX + str(generation) + ".rotate_tmp"): return _failure( &"AUTOSAVE_RECONCILE_ROLLBACK_FAILED", "generation %d remained held" % generation ) return { &"ok": true, &"code": &"OK", &"reconciled": true, &"action": &"rolled_back", } # The new generation zero is already visible: complete every remaining # oldest-to-newest shift. Preflight conflicts before making any rename. for generation in held_generations: if generation >= generation_count - 1: continue var destination_path := target_path + AUTOSAVE_SUFFIX + str(generation + 1) if DirAccess.dir_exists_absolute(destination_path): return _failure( &"AUTOSAVE_ROTATION_AMBIGUOUS", "held generation %d conflicts with destination" % generation ) for generation in held_generations: var hold_path := target_path + AUTOSAVE_SUFFIX + str(generation) + ".rotate_tmp" if generation >= generation_count - 1: continue var destination_path := target_path + AUTOSAVE_SUFFIX + str(generation + 1) var shift_error := DirAccess.rename_absolute(hold_path, destination_path) if shift_error != OK: return _failure(&"AUTOSAVE_RECONCILE_SHIFT_FAILED", error_string(shift_error)) for generation in held_generations: _remove_tree(target_path + AUTOSAVE_SUFFIX + str(generation) + ".rotate_tmp") return { &"ok": true, &"code": &"OK", &"reconciled": true, &"action": &"completed", } static func _rollback_autosave_holds(target_path: String, generations: Array[int]) -> void: for generation in generations: var transaction_path := target_path + AUTOSAVE_SUFFIX + str(generation) + ".rotate_tmp" var generation_path := target_path + AUTOSAVE_SUFFIX + str(generation) if DirAccess.dir_exists_absolute(transaction_path) and not DirAccess.dir_exists_absolute(generation_path): DirAccess.rename_absolute(transaction_path, generation_path) ## Resolves the two durable migration markers. The source hold is never removed ## until a current target and a checksum-valid pre-migration backup both exist. static func _reconcile_interrupted_migration(target_path: String) -> Dictionary: var candidate_path := target_path + ".migration.tmp" var original_path := target_path + ".migration.original" var backup_path := target_path + MIGRATION_BACKUP_SUFFIX var backup_temp_path := backup_path + ".tmp" var backup_old_path := backup_path + ".old" var operation_paths: Array[String] = [ target_path, candidate_path, original_path, backup_path, backup_temp_path, backup_old_path, ] if not _operation_paths_are_safe(operation_paths): return _failure(&"UNSAFE_PATH", "migration recovery path contains a symbolic link") var original_status := _package_status(original_path) var target_status := _package_status(target_path) var candidate_status := _package_status(candidate_path) var backup_status := _package_status(backup_path) var backup_temp_status := _package_status(backup_temp_path) var backup_old_status := _package_status(backup_old_path) # Recover a backup rename that was interrupted before the formal source moved. if not backup_status[&"exists"]: if backup_temp_status[&"package_valid"]: var temp_promote_error := DirAccess.rename_absolute(backup_temp_path, backup_path) if temp_promote_error != OK: return _failure(&"MIGRATION_BACKUP_RECOVERY_FAILED", error_string(temp_promote_error)) backup_status = _package_status(backup_path) elif backup_old_status[&"package_valid"]: var old_restore_error := DirAccess.rename_absolute(backup_old_path, backup_path) if old_restore_error != OK: return _failure(&"MIGRATION_BACKUP_RECOVERY_FAILED", error_string(old_restore_error)) backup_status = _package_status(backup_path) elif backup_status[&"package_valid"]: if backup_temp_status[&"package_valid"]: _remove_tree(backup_temp_path) if backup_old_status[&"package_valid"] and not original_status[&"exists"]: _remove_tree(backup_old_path) else: # A torn backup package never outranks either verified durable neighbor. # It is removed only when it still has recognizable package contents. if not _looks_like_save_package(backup_path): return _failure(&"MIGRATION_BACKUP_PATH_CONFLICT", backup_path) if backup_temp_status[&"package_valid"]: _remove_tree(backup_path) var temp_replace_error := DirAccess.rename_absolute(backup_temp_path, backup_path) if temp_replace_error != OK: return _failure(&"MIGRATION_BACKUP_RECOVERY_FAILED", error_string(temp_replace_error)) backup_status = _package_status(backup_path) elif backup_old_status[&"package_valid"]: _remove_tree(backup_path) var old_replace_error := DirAccess.rename_absolute(backup_old_path, backup_path) if old_replace_error != OK: return _failure(&"MIGRATION_BACKUP_RECOVERY_FAILED", error_string(old_replace_error)) backup_status = _package_status(backup_path) if original_status[&"exists"]: if not original_status[&"package_valid"]: return _failure( &"MIGRATION_RECOVERY_SOURCE_INVALID", str(original_status.get(&"code", &"INVALID_PACKAGE")) ) if target_status[&"current_loadable"]: if not backup_status[&"package_valid"]: var backup_result := _create_verified_migration_backup( original_path, backup_path, backup_temp_path, backup_old_path ) if not backup_result[&"ok"]: return backup_result _remove_tree(original_path) if candidate_status[&"package_valid"]: _remove_tree(candidate_path) if DirAccess.dir_exists_absolute(backup_old_path): _remove_tree(backup_old_path) return { &"ok": true, &"code": &"OK", &"reconciled": true, &"action": &"finalized_promoted_target", } if not target_status[&"exists"] and candidate_status[&"current_loadable"] \ and backup_status[&"package_valid"]: var candidate_promote_error := DirAccess.rename_absolute(candidate_path, target_path) if candidate_promote_error != OK: return _failure(&"MIGRATION_RECOVERY_PROMOTE_FAILED", error_string(candidate_promote_error)) var promoted_status := _package_status(target_path) if not promoted_status[&"current_loadable"]: _remove_tree(target_path) DirAccess.rename_absolute(original_path, target_path) return _failure(&"MIGRATION_RECOVERY_VERIFY_FAILED", target_path) _remove_tree(original_path) if DirAccess.dir_exists_absolute(backup_old_path): _remove_tree(backup_old_path) return { &"ok": true, &"code": &"OK", &"reconciled": true, &"action": &"completed_promotion", } # The candidate cannot be safely committed. Restore the verified original. if target_status[&"exists"]: if not _looks_like_save_package(target_path): return _failure(&"MIGRATION_RECOVERY_TARGET_CONFLICT", target_path) _remove_tree(target_path) var rollback_error := DirAccess.rename_absolute(original_path, target_path) if rollback_error != OK: return _failure(&"MIGRATION_RECOVERY_ROLLBACK_FAILED", error_string(rollback_error)) if candidate_status[&"package_valid"]: _remove_tree(candidate_path) if backup_old_status[&"package_valid"]: _restore_previous_migration_backup(backup_path, backup_old_path) return { &"ok": true, &"code": &"OK", &"reconciled": true, &"action": &"rolled_back_to_original", } # A current formal package makes a pre-hold candidate disposable. If the # formal package vanished, a verified candidate plus backup can still commit. if target_status[&"current_loadable"]: if candidate_status[&"package_valid"]: _remove_tree(candidate_path) return {&"ok": true, &"code": &"OK", &"reconciled": false, &"action": &"none"} if not target_status[&"exists"] and candidate_status[&"current_loadable"] \ and backup_status[&"package_valid"]: var orphan_promote_error := DirAccess.rename_absolute(candidate_path, target_path) if orphan_promote_error != OK: return _failure(&"MIGRATION_RECOVERY_PROMOTE_FAILED", error_string(orphan_promote_error)) return { &"ok": true, &"code": &"OK", &"reconciled": true, &"action": &"promoted_verified_candidate", } return {&"ok": true, &"code": &"OK", &"reconciled": false, &"action": &"none"} static func _create_verified_migration_backup( source_path: String, backup_path: String, backup_temp_path: String, backup_old_path: String ) -> Dictionary: _remove_tree(backup_temp_path) var copy_error := _copy_tree(source_path, backup_temp_path) if copy_error != OK: return _failure(&"MIGRATION_BACKUP_COPY_FAILED", error_string(copy_error)) var verification := _load_snapshot_absolute(backup_temp_path, -1, false) if not verification[&"ok"]: _remove_tree(backup_temp_path) return _failure(&"MIGRATION_BACKUP_VERIFY_FAILED", str(verification[&"code"])) if DirAccess.dir_exists_absolute(backup_path): if DirAccess.dir_exists_absolute(backup_old_path): return _failure(&"MIGRATION_BACKUP_RECOVERY_AMBIGUOUS", backup_old_path) var rotate_error := DirAccess.rename_absolute(backup_path, backup_old_path) if rotate_error != OK: return _failure(&"MIGRATION_BACKUP_ROTATE_FAILED", error_string(rotate_error)) var promote_error := DirAccess.rename_absolute(backup_temp_path, backup_path) if promote_error != OK: if DirAccess.dir_exists_absolute(backup_old_path): DirAccess.rename_absolute(backup_old_path, backup_path) return _failure(&"MIGRATION_BACKUP_PROMOTE_FAILED", error_string(promote_error)) return {&"ok": true, &"code": &"OK"} static func _package_status(path: String) -> Dictionary: var exists := DirAccess.dir_exists_absolute(path) or FileAccess.file_exists(path) if not exists: return { &"exists": false, &"package_valid": false, &"current_loadable": false, &"code": &"SAVE_NOT_FOUND", } var loaded := _load_snapshot_absolute(path, -1, false) if not loaded[&"ok"]: return { &"exists": true, &"package_valid": false, &"current_loadable": false, &"code": loaded[&"code"], } var current_format := int(loaded[&"manifest"].get("format_version", -1)) == FORMAT_VERSION var schema_valid := bool(SimulationSnapshot.validate(loaded[&"snapshot"])[&"ok"]) return { &"exists": true, &"package_valid": true, &"current_loadable": current_format and schema_valid, &"code": &"OK" if current_format and schema_valid else &"REQUIRES_MIGRATION", &"format_version": int(loaded[&"manifest"].get("format_version", -1)), } static func _restore_previous_migration_backup(backup_path: String, old_backup_path: String) -> void: _remove_tree(backup_path) if DirAccess.dir_exists_absolute(old_backup_path): DirAccess.rename_absolute(old_backup_path, backup_path) static func _copy_tree(source_path: String, destination_path: String) -> Error: if not DirAccess.dir_exists_absolute(source_path): return ERR_DOES_NOT_EXIST if not _operation_paths_are_safe([source_path, destination_path]): return ERR_UNAUTHORIZED _remove_tree(destination_path) var make_error := DirAccess.make_dir_recursive_absolute(destination_path) if make_error != OK: return make_error var source_directory := DirAccess.open(source_path) if source_directory == null: return DirAccess.get_open_error() source_directory.list_dir_begin() var name := source_directory.get_next() while not name.is_empty(): if name != "." and name != "..": var source_child := source_path.path_join(name) var destination_child := destination_path.path_join(name) var copy_error := OK if source_directory.current_is_dir(): copy_error = _copy_tree(source_child, destination_child) else: copy_error = DirAccess.copy_absolute(source_child, destination_child) if copy_error != OK: source_directory.list_dir_end() _remove_tree(destination_path) return copy_error name = source_directory.get_next() source_directory.list_dir_end() return OK static func _resolve_safe_save_path(save_path: String) -> Dictionary: if save_path.is_empty(): return _failure(&"INVALID_PATH", save_path) var user_root := ProjectSettings.globalize_path("user://").simplify_path() var target_path := "" if save_path.begins_with("user://"): var relative_path := save_path.trim_prefix("user://") if relative_path.is_empty() or relative_path.is_absolute_path(): return _failure(&"INVALID_PATH", save_path) var segments := relative_path.replace("\\", "/").split("/", false) for segment in segments: if segment == "." or segment == ".." or segment.is_empty(): return _failure(&"UNSAFE_PATH", save_path) target_path = ProjectSettings.globalize_path(save_path).simplify_path() elif save_path.is_absolute_path(): target_path = save_path.simplify_path() else: return _failure(&"INVALID_PATH", save_path) if target_path == user_root or not target_path.begins_with(user_root + "/"): return _failure(&"UNSAFE_PATH", save_path) if _path_has_symbolic_link_component(target_path, user_root): return _failure(&"UNSAFE_PATH", "symbolic link component: %s" % save_path) return {&"ok": true, &"code": &"OK", &"path": target_path} static func _write_world( path: String, uncompressed_size: int, compressed_world: PackedByteArray, format_version: int ) -> Error: var file := FileAccess.open(path, FileAccess.WRITE) if file == null: return FileAccess.get_open_error() file.store_buffer(MAGIC.to_ascii_buffer()) file.store_32(format_version) file.store_64(uncompressed_size) file.store_buffer(compressed_world) return OK static func _write_text(path: String, content: String) -> Error: var file := FileAccess.open(path, FileAccess.WRITE) if file == null: return FileAccess.get_open_error() file.store_string(content) return OK static func _write_formal_transaction_marker(path: String) -> Error: var file := FileAccess.open(path, FileAccess.WRITE) if file == null: return FileAccess.get_open_error() file.store_string(SAVE_TRANSACTION_MARKER) file.flush() return OK static func _sha256(bytes: PackedByteArray) -> String: var context := HashingContext.new() var start_error := context.start(HashingContext.HASH_SHA256) if start_error != OK: return "" context.update(bytes) return context.finish().hex_encode() static func _complete_timed_operation( result: Dictionary, metric_id: StringName, started_at_us: int ) -> Dictionary: var duration_us := maxi(int(Time.get_ticks_usec() - started_at_us), 0) _performance_budget.record_sample(metric_id, duration_us) var timed := result.duplicate(true) timed[&"duration_us"] = duration_us return timed static func _read_manifest_identity(package_path: String) -> Dictionary: var manifest_path := package_path.path_join(MANIFEST_FILE) if not FileAccess.file_exists(manifest_path): return {} var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(manifest_path)) if not parsed is Dictionary: return {} var manifest: Dictionary = parsed var identity := {} for key: StringName in [&"save_id", &"display_name", &"created_at_real"]: if manifest.has(key) or manifest.has(String(key)): identity[key] = manifest.get(key, manifest.get(String(key))) return identity static func _merge_save_metadata( metadata: Dictionary, prior_identity: Dictionary, logical_target_path: String ) -> Dictionary: var merged := prior_identity.duplicate(true) for key: Variant in metadata: merged[key] = metadata[key] var logical_name := logical_target_path.get_file() if str(merged.get(&"save_id", "")).is_empty(): merged[&"save_id"] = logical_name if str(merged.get(&"display_name", "")).is_empty(): merged[&"display_name"] = logical_name return merged static func _metadata_array(value: Variant) -> Array: if value is Array: return value.duplicate(true) if value is PackedStringArray: var unpacked: Array = [] for item: String in value: unpacked.append(item) return unpacked return [] static func _snapshot_seed(snapshot: Dictionary) -> int: var systems_value: Variant = snapshot.get(&"systems", {}) if not systems_value is Dictionary: return 0 var systems: Dictionary = systems_value var runtime_value: Variant = systems.get(&"runtime", {}) if runtime_value is Dictionary: return int(runtime_value.get(&"world_seed", 0)) return int(systems.get(&"world_seed", 0)) static func _ensure_thumbnail(package_path: String, metadata: Dictionary) -> Error: var thumbnail_path := package_path.path_join(THUMBNAIL_FILE) var supplied_bytes := PackedByteArray() for key: StringName in [&"thumbnail_png", &"_thumbnail_png"]: var value: Variant = metadata.get(key, null) if value is PackedByteArray and not value.is_empty(): supplied_bytes = value.duplicate() break if supplied_bytes.is_empty(): var supplied_path := str(metadata.get(&"thumbnail_path", "")) if not supplied_path.is_empty() and FileAccess.file_exists(supplied_path): supplied_bytes = FileAccess.get_file_as_bytes(supplied_path) if not supplied_bytes.is_empty(): var supplied_image := Image.new() var decode_error := supplied_image.load_png_from_buffer(supplied_bytes) if decode_error != OK: return decode_error var file := FileAccess.open(thumbnail_path, FileAccess.WRITE) if file == null: return FileAccess.get_open_error() file.store_buffer(supplied_bytes) return OK var placeholder := Image.create_empty(1, 1, false, Image.FORMAT_RGBA8) placeholder.fill(Color(0.08, 0.12, 0.10, 1.0)) return placeholder.save_png(thumbnail_path) static func _looks_like_save_package(path: String) -> bool: if not DirAccess.dir_exists_absolute(path): return false if FileAccess.file_exists(path.path_join(WORLD_FILE)): return true var has_manifest := FileAccess.file_exists(path.path_join(MANIFEST_FILE)) var has_companion := ( FileAccess.file_exists(path.path_join(THUMBNAIL_FILE)) or FileAccess.file_exists(path.path_join(JOURNAL_FILE)) ) return has_manifest and has_companion static func _path_is_missing_or_package(path: String) -> bool: if not FileAccess.file_exists(path) and not DirAccess.dir_exists_absolute(path): return true return _looks_like_save_package(path) static func _validate_package_read_boundary(package_path: String) -> Dictionary: if not _operation_paths_are_safe([package_path]): return _failure(&"UNSAFE_PATH", "save package contains a symbolic link: %s" % package_path) if not DirAccess.dir_exists_absolute(package_path): return _failure(&"SAVE_NOT_FOUND", package_path) for file_name: String in [WORLD_FILE, MANIFEST_FILE, THUMBNAIL_FILE, JOURNAL_FILE]: var known_path := package_path.path_join(file_name) if _is_symbolic_link(known_path): return _failure(&"UNSAFE_PATH", "symbolic link in save package: %s" % known_path) if DirAccess.dir_exists_absolute(known_path): return _failure(&"INVALID_PACKAGE_ENTRY", known_path) return {&"ok": true, &"code": &"OK"} static func _package_file_is_safe_for_read(path: String) -> bool: var user_root := ProjectSettings.globalize_path("user://").simplify_path() var normalized := path.simplify_path() if normalized == user_root or not normalized.begins_with(user_root + "/"): return false if _path_has_symbolic_link_component(normalized, user_root): return false return not _is_symbolic_link(normalized) and not DirAccess.dir_exists_absolute(normalized) static func _formal_transaction_marker_is_valid(marker_path: String) -> bool: if _is_symbolic_link(marker_path) or DirAccess.dir_exists_absolute(marker_path): return false if not FileAccess.file_exists(marker_path): return false return FileAccess.get_file_as_string(marker_path) == SAVE_TRANSACTION_MARKER static func _remove_formal_transaction_marker(marker_path: String) -> void: if _formal_transaction_marker_is_valid(marker_path): DirAccess.remove_absolute(marker_path) static func _operation_paths_are_safe(paths: Array) -> bool: var user_root := ProjectSettings.globalize_path("user://").simplify_path() for raw_path: Variant in paths: var path := str(raw_path).simplify_path() if path == user_root or not path.begins_with(user_root + "/"): return false if _path_has_symbolic_link_component(path, user_root): return false if DirAccess.dir_exists_absolute(path) and _tree_contains_symbolic_link(path): return false return true static func _path_has_symbolic_link_component(path: String, root_path: String) -> bool: if path == root_path: return false var relative := path.trim_prefix(root_path + "/") var current := root_path for segment: String in relative.split("/", false): var parent := DirAccess.open(current) if parent != null and parent.is_link(segment): return true current = current.path_join(segment) return false static func _tree_contains_symbolic_link(path: String) -> bool: var directory := DirAccess.open(path) if directory == null: return false directory.list_dir_begin() var name := directory.get_next() while not name.is_empty(): if name != "." and name != "..": if directory.is_link(name): directory.list_dir_end() return true if directory.current_is_dir() and _tree_contains_symbolic_link(path.path_join(name)): directory.list_dir_end() return true name = directory.get_next() directory.list_dir_end() return false static func _is_symbolic_link(path: String) -> bool: var parent := DirAccess.open(path.get_base_dir()) return parent != null and parent.is_link(path.get_file()) static func _with_context(result: Dictionary, context: Dictionary) -> Dictionary: var enriched := result.duplicate(true) for key: Variant in context: enriched[key] = context[key] return enriched static func _failure(code: StringName, message: String) -> Dictionary: return { &"ok": false, &"code": code, &"message": message, } static func _remove_tree(path: String) -> void: if _is_symbolic_link(path): DirAccess.remove_absolute(path) return if FileAccess.file_exists(path): DirAccess.remove_absolute(path) return if not DirAccess.dir_exists_absolute(path): return var directory := DirAccess.open(path) if directory == null: return directory.list_dir_begin() var name := directory.get_next() while not name.is_empty(): if name != "." and name != "..": var child_path := path.path_join(name) if directory.current_is_dir(): _remove_tree(child_path) else: DirAccess.remove_absolute(child_path) name = directory.get_next() directory.list_dir_end() DirAccess.remove_absolute(path)