extends SceneTree const MainScene: PackedScene = preload("res://main/main.tscn") const WAIT_MILLISECONDS: int = 20_000 var _conflicts: Array[Dictionary] = [] func _initialize() -> void: _run.call_deferred() func _run() -> void: root.size = Vector2i(1280, 720) var main: Node = MainScene.instantiate() root.add_child(main) for _frame: int in 4: await process_frame if not bool(main.get("_application_initialized")): main.call("_activate_selected_data_path", "", true) for _frame: int in 8: await process_frame assert(bool(main.get("_application_initialized"))) var data_root := main.get("_data_root") as PlayerDataRoot var save_manager := main.get("_save_manager") as PlayerSaveManager var save_slots := main.get("_save_slots") as PlayerSaveSlotCatalog var player := main.get("_player") as Player var session := main.get_node("%NetworkSession") as NetworkSession var network_profile := main.get_node( "%NetworkProfilePreferences" ) as NetworkProfilePreferences var appearance_store := main.get_node( "%PlayerAppearanceStore" ) as PlayerAppearanceStore assert( data_root != null and save_manager != null and save_slots != null and player != null and session != null and network_profile != null and appearance_store != null ) var conflict_dialog_callback := Callable(main, "_on_portable_conflict") if data_root.conflict_detected.is_connected(conflict_dialog_callback): data_root.conflict_detected.disconnect(conflict_dialog_callback) data_root.conflict_detected.connect(_on_conflict_detected) if OS.get_environment( PlayerDataRoot.ISOLATED_VALIDATION_ENVIRONMENT ) == "1": assert( PlayerDataRoot.ISOLATED_VALIDATION_DIRECTORY in data_root.root_path ) main.call( "_on_new_slot_requested", "reload regression", WorldLayout.STARTER_ISLAND, 424242, "", ) assert(await _wait_for_gameplay(main, true)) var slot_id: String = save_slots.get_active_slot_id() assert(not slot_id.is_empty()) assert(player.wallet.restore_balance(321)) assert(network_profile.set_profile_identity( "reload stray", "deep", network_profile.speech_speed_id, network_profile.call_id, network_profile.sample_set_id, "returner", )) var custom_appearance: Dictionary = appearance_store.get_snapshot() custom_appearance["scale"] = 0.8 assert(appearance_store.save_snapshot(custom_appearance)) main.call("_on_return_to_title_requested") assert(await _wait_for_gameplay(main, false)) assert(not save_manager.is_dirty()) # Establish the exact reported lifecycle once: load a local slot, return to # title, then immediately select that same slot again in this process. main.call("_on_slot_play_requested", slot_id) assert(await _wait_for_gameplay(main, true)) main.call("_on_return_to_title_requested") assert(await _wait_for_gameplay(main, false)) assert(not save_manager.is_dirty()) # A selected save owns its character state. An out-of-band edit to the # compatibility profile mirror must not make that valid save unloadable or # be overwritten merely by loading it. A later explicit profile edit must # still detect the external mutation. var profile_path: String = data_root.path_for(&"network_profile") var external_profile_bytes := PortableFileGuard.read_bytes(profile_path) assert(not external_profile_bytes.is_empty()) external_profile_bytes.append(0x0a) assert(_write_external_bytes(profile_path, external_profile_bytes)) _conflicts.clear() main.call("_on_slot_play_requested", slot_id) assert(await _wait_for_gameplay(main, true)) assert( _conflicts.is_empty(), "same-process same-slot reload reported false conflicts: %s" % [_conflicts], ) assert(player.wallet.get_balance() == 321) assert(network_profile.display_name == "reload stray") assert(network_profile.title == "returner") assert(appearance_store.get_snapshot() == custom_appearance) assert( PortableFileGuard.read_bytes(profile_path) == external_profile_bytes ) _conflicts.clear() assert(not network_profile.set_display_name("explicit overwrite")) assert(_conflicts.size() == 1) assert(FileAccess.file_exists(str(_conflicts.front().get("path", "")))) # A real out-of-band edit must still be caught. Mutating the canonical bytes # after load simulates a Syncthing/device replacement while this process is # holding an older expected hash. var save_path: String = str(save_manager.get("_save_path")) var external_bytes := PortableFileGuard.read_bytes(save_path) assert(not external_bytes.is_empty()) external_bytes.append(0x7f) assert(_write_external_bytes(save_path, external_bytes)) assert(player.wallet.restore_balance(654)) _conflicts.clear() assert(not save_manager.save_now()) assert(_conflicts.size() == 1) assert(FileAccess.file_exists(str(_conflicts.front().get("path", "")))) save_manager.set_autosave_enabled(false) save_manager.cancel_pending_autosave() session.disconnect_session("Save reload conflict validation complete.") main.queue_free() for _frame: int in 4: await process_frame print("Save reload conflict validation: PASS") quit(0) func _wait_for_gameplay(main: Node, expected: bool) -> bool: var deadline: int = Time.get_ticks_msec() + WAIT_MILLISECONDS while Time.get_ticks_msec() < deadline: if bool(main.get("_gameplay_started")) == expected: return true await process_frame return false func _on_conflict_detected(message: String, path: String) -> void: _conflicts.append({"message": message, "path": path}) func _write_external_bytes(path: String, bytes: PackedByteArray) -> bool: var file := FileAccess.open(path, FileAccess.WRITE) if file == null: return false file.store_buffer(bytes) file.flush() var succeeded: bool = file.get_error() == OK file.close() return succeeded