straywild/tests/save_reload_conflict_validation.gd

167 lines
5.6 KiB
GDScript3
Raw Normal View History

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