167 lines
5.6 KiB
GDScript3
167 lines
5.6 KiB
GDScript3
|
|
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
|