extends SceneTree var failures: Array[String] = [] func _initialize() -> void: call_deferred("_run") func _run() -> void: var test_directory := "user://save_slot_test_%s" % Time.get_ticks_usec() var store := SaveSlotStore.new(test_directory) var manager: Node = load("res://simulation/SimulationManager.gd").new() manager.debug_logs = false root.add_child(manager) manager.set_process(false) for step in 8: manager.simulate_tick() _check( manager.request_regional_jajce_delivery(3.0), "The quicksave fixture should start one production regional delivery", ) var saved_checksum: String = manager.get_state_checksum() var saved_regional_checksum: String = manager.get_regional_state_checksum() var saved_world_checksum: String = manager.get_world_state_checksum() _check(store.save(manager), "A valid simulation should save atomically") _check(store.has_slot(), "The validated quicksave should be discoverable") for step in 5: manager.simulate_tick() _check( manager.get_state_checksum() != saved_checksum, "The scenario should change after the save point" ) var restore_notifications: Array[StringName] = [] manager.village_changed.connect( func(_village: SimVillage) -> void: restore_notifications.append(&"village") ) manager.state_restored.connect(func() -> void: restore_notifications.append(&"state")) _check(store.load_into(manager), "A valid quicksave should load") _check( restore_notifications == [&"village", &"state"], "A committed combined restore should publish its two notifications exactly once", ) _check( manager.get_state_checksum() == saved_checksum, "Loading should restore the exact saved simulation" ) _check( manager.get_regional_state_checksum() == saved_regional_checksum, "Loading should restore the exact mid-route regional authority", ) _check( manager.get_world_state_checksum() == saved_world_checksum, "Loading should restore both authorities from the same manifest", ) var final_path := store.get_slot_path() var backup_path := final_path + ".bak" _check( DirAccess.rename_absolute(final_path, backup_path) == OK, "Test should reproduce the atomic replacement recovery window" ) _check( store.load_into(manager), "A preserved previous save should recover an interrupted replacement" ) _check( manager.get_world_state_checksum() == saved_world_checksum, "Backup recovery should restore the complete combined manifest", ) _check( DirAccess.rename_absolute(backup_path, final_path) == OK, "Test should restore the primary save path" ) var legacy_slot := "legacy_v16" var legacy_path := store.get_slot_path(legacy_slot) var legacy_local_checksum: String = manager.get_state_checksum() var legacy_file := FileAccess.open(legacy_path, FileAccess.WRITE) _check(legacy_file != null, "Test should create a raw legacy local save") if legacy_file != null: legacy_file.store_string(manager.serialize_state()) legacy_file.close() manager.simulate_tick() _check( store.load_into(manager, legacy_slot), "Existing raw v16 quicksaves should remain loadable", ) _check( manager.get_state_checksum() == legacy_local_checksum, "A legacy quicksave should restore its exact local authority", ) var legacy_regional := RegionalSimulationFacade.from_dictionary(manager.get_regional_snapshot()) _check(legacy_regional != null, "Legacy load should bootstrap a valid regional facade") if legacy_regional != null: var legacy_service := legacy_regional.get_service() var legacy_caravan := legacy_service.get_world_state().get_mobile_group( SimulationIds.REGIONAL_CARAVAN_BLUE ) _check( legacy_regional.get_last_processed_tick() == manager.tick_count, "Legacy regional bootstrap should align with the saved local tick", ) _check( legacy_regional.get_world_seed() == manager.simulation_seed, "Legacy regional bootstrap should align with the saved world seed", ) _check( ( legacy_service.get_scheduler().is_empty() and legacy_service.get_event_store().is_empty() ), "Legacy load must not invent a historical regional journey", ) _check( ( legacy_caravan != null and legacy_caravan.is_at_location() and ( legacy_caravan.get_current_location_id() == SimulationIds.REGIONAL_LOCATION_TRAVNIK ) ), "Legacy load should place the idle authored caravan at Travnik", ) store.delete_slot(legacy_slot) var custom_adapter := ActiveWorldAdapter.new() _check( custom_adapter.configure_context_identity( &"save_test_context", &"save_test_world", &"save_test_location" ), "The custom-scope fixture should configure an independent active context", ) root.add_child(custom_adapter) var custom_manager: Node = load("res://simulation/SimulationManager.gd").new() custom_manager.debug_logs = false custom_manager.active_world_adapter = custom_adapter root.add_child(custom_manager) custom_manager.set_process(false) var custom_slot := "custom_scope" var custom_saved_local_checksum: String = custom_manager.get_state_checksum() var custom_saved_regional_checksum: String = custom_manager.get_regional_state_checksum() _check( not custom_manager.uses_combined_save_manifest(), "A non-Jajce adapter scope should keep the supported local-v16 save boundary", ) _check( custom_manager.can_serialize_local_state_only(), "A custom scope should allow local-only saving while regional state is reconstructibly idle", ) _check( ( not custom_manager.request_regional_jajce_delivery(1.0) and custom_manager.get_regional_state_checksum() == custom_saved_regional_checksum ), "A non-Jajce adapter scope must reject Jajce regional mutation without changing authority", ) _check( store.save(custom_manager, custom_slot), "A supported non-Jajce adapter scope should still save through SaveSlotStore", ) var custom_file := FileAccess.open(store.get_slot_path(custom_slot), FileAccess.READ) _check(custom_file != null, "The custom-scope slot should be readable") if custom_file != null: var custom_json := custom_file.get_as_text() custom_file.close() _check( ( SimulationSaveManifest.from_json(custom_json) == null and SimulationStateRecord.from_json(custom_json) != null ), "A non-Jajce adapter scope should persist the explicit raw local record", ) custom_manager.simulate_tick() _check( store.load_into(custom_manager, custom_slot), "The custom-scope raw local record should remain loadable", ) _check( custom_manager.get_state_checksum() == custom_saved_local_checksum, "Custom-scope loading should restore the exact local authority", ) _check( custom_manager.get_regional_state_checksum() == custom_saved_regional_checksum, "Custom-scope loading should reconstruct the exact idle regional facade", ) var rejected_restore_notifications: Array[StringName] = [] custom_manager.village_changed.connect( func(_village: SimVillage) -> void: rejected_restore_notifications.append(&"village") ) custom_manager.state_restored.connect( func() -> void: rejected_restore_notifications.append(&"state") ) var custom_before_rejected_local: String = custom_manager.get_state_checksum() var custom_before_rejected_regional: String = custom_manager.get_regional_state_checksum() _check( not custom_manager.restore_save_manifest(manager.create_save_manifest()), "A Jajce manifest should be rejected by a different active adapter scope", ) _check( rejected_restore_notifications.is_empty(), "A rejected combined restore and its rollback must publish no success notifications", ) _check( ( custom_manager.get_state_checksum() == custom_before_rejected_local and custom_manager.get_regional_state_checksum() == custom_before_rejected_regional ), "A rejected combined restore should roll back both custom-scope authorities", ) _check( custom_manager._regional_simulation.depart_jajce_delivery(1.0), "The fail-closed fixture should create otherwise unreachable live regional state", ) var custom_live_regional_checksum: String = custom_manager.get_regional_state_checksum() _check( not custom_manager.can_serialize_local_state_only(), "Live regional state must not be treated as reconstructible local-only authority", ) _check( not store.save(custom_manager, "custom_live"), "SaveSlotStore must reject a local-only save that would discard live regional state", ) _check( custom_manager.get_regional_state_checksum() == custom_live_regional_checksum, "Rejecting the lossy custom-scope save must not mutate regional authority", ) store.delete_slot(custom_slot) var unchanged_world_checksum: String = manager.get_world_state_checksum() var rejected_manifest: Dictionary = manager.create_save_manifest().to_dictionary() rejected_manifest["regional_state"]["active_location_id"] = String( SimulationIds.REGIONAL_LOCATION_TRAVNIK ) rejected_manifest["regional_checksum"] = SimulationSaveManifest._checksum_dictionary( rejected_manifest["regional_state"] ) var file := FileAccess.open(final_path, FileAccess.WRITE) _check(file != null, "Test should open the primary save for corruption") if file != null: file.store_string(JSON.stringify(rejected_manifest)) file.close() _check(not store.load_into(manager), "Invalid regional manifest data should be rejected") _check( manager.get_world_state_checksum() == unchanged_world_checksum, "A rejected save must not partially mutate either authority", ) _check(not store.save(manager, "../escape"), "Slot names must not escape the save directory") store.delete_slot() DirAccess.remove_absolute(ProjectSettings.globalize_path(test_directory)) if failures.is_empty(): print("[TEST] Save-slot persistence passed") quit(0) return for failure in failures: push_error("[TEST] " + failure) quit(1) func _check(condition: bool, message: String) -> void: if not condition: failures.append(message)