Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
175 lines
9.1 KiB
GDScript
175 lines
9.1 KiB
GDScript
extends SceneTree
|
|
const FixtureCleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd")
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _runtime: Variant
|
|
var _clock: Variant
|
|
var _state: Variant
|
|
var _main: Control
|
|
var _path := "user://tests/selection_lifecycle_%d/slot" % OS.get_process_id()
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
func _expect(ok: bool, message: String) -> void:
|
|
_checks += 1
|
|
if not ok:
|
|
_failures += 1
|
|
push_error(message)
|
|
|
|
func _mount() -> void:
|
|
_main = load("res://scenes/main/main.tscn").instantiate()
|
|
_main.set("manual_save_path", _path)
|
|
root.add_child(_main)
|
|
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
|
|
await process_frame
|
|
await process_frame
|
|
|
|
func _build(id: StringName, cell: Vector3i) -> StringName:
|
|
_main.call("_set_layer", cell.z)
|
|
_main.call("_select_build_definition", id)
|
|
var before: int = _runtime.construction_service.get_jobs().size()
|
|
_main.call("_on_map_cell_clicked", cell)
|
|
var jobs: Array = _runtime.construction_service.get_jobs()
|
|
_expect(jobs.size() == before + 1, "normal palette creates %s construction" % id)
|
|
return jobs.back().job_id if jobs.size() == before + 1 else &""
|
|
|
|
func _round_trip(kind: StringName, id: StringName, heading: String) -> void:
|
|
_main.call("_save_manual")
|
|
var package := SaveService.load_snapshot(_path)
|
|
_expect(package.get(&"ok", false), "%s selection has a valid package" % kind)
|
|
_main.set("_selected_entity_id", &"")
|
|
_main.set("_selected_entity_kind", &"")
|
|
_main.call("_load_manual", true)
|
|
_expect(_main.get("_selected_entity_id") == id and _main.get("_selected_entity_kind") == kind, "%s selection restores its actual target" % kind)
|
|
_expect(heading in _main.get("selected_heading_label").text, "%s inspector restores the target details" % kind)
|
|
var expected_highlight := &"" if kind == &"excavation" else id
|
|
_expect(_main.get("world_preview").selected_entity_id == expected_highlight, "%s map selection uses the appropriate highlight" % kind)
|
|
|
|
func _run() -> void:
|
|
_runtime = root.get_node("SimulationRuntime")
|
|
_clock = root.get_node("GameClock")
|
|
_state = root.get_node("GameState")
|
|
_clock.reset(1, 6, 0)
|
|
_state.reset()
|
|
_runtime.initialize(_clock, _state, 42530)
|
|
_clock.set_paused(true)
|
|
root.size = Vector2i(1440, 810)
|
|
await _mount()
|
|
var reception_job := _build(&"reception", Vector3i(-8, 0, 0))
|
|
if reception_job.is_empty():
|
|
_finish()
|
|
return
|
|
_main.call("_clear_build_selection")
|
|
_main.call("_on_construction_selected", reception_job)
|
|
_round_trip(&"construction", reception_job, "工事")
|
|
_clock.advance_minutes(120)
|
|
var reception_id: StringName = _runtime.construction_service.get_facilities()[0][&"instance_id"]
|
|
_main.call("_on_facility_selected", reception_id)
|
|
_round_trip(&"facility", reception_id, "受付")
|
|
var stair_job := _build(&"guest_stairs", Vector3i(0, 1, 0))
|
|
_clock.advance_minutes(60)
|
|
_main.call("_clear_build_selection")
|
|
var stair_id := &""
|
|
for row: Dictionary in _runtime.construction_service.get_facilities():
|
|
if row[&"definition_id"] == &"guest_stairs":
|
|
stair_id = row[&"instance_id"]
|
|
_expect(not stair_job.is_empty() and not stair_id.is_empty(), "paid stair construction finishes without a FacilityService instance")
|
|
if not stair_id.is_empty():
|
|
_main.call("_on_facility_selected", stair_id)
|
|
_round_trip(&"facility", stair_id, "客用階段")
|
|
# Rank is an explicit boundary fixture; this suite does not claim natural rank progression.
|
|
_runtime.submit_command(&"update_progression_metrics", {&"metrics": {&"reputation": 25}}, &"player", &"", 100, -1, &"selection.rank.reputation")
|
|
_runtime.submit_command(&"change_resources", {&"deltas": {&"gold": 10000}}, &"player", &"", 100, -1, &"selection.rank.capital")
|
|
_expect(_runtime.submit_command(&"set_world_tree_rank", {&"rank": 2}, &"player", &"", 100, -1, &"selection.rank.promote").is_success(), "boundary fixture unlocks excavation")
|
|
_main.call("_set_excavation_mode", true)
|
|
_main.call("_on_map_cell_clicked", Vector3i(0, -1, 0))
|
|
var excavations: Array = _runtime.excavation_service.get_jobs()
|
|
_expect(not excavations.is_empty(), "normal excavation UI creates a saved job")
|
|
if not excavations.is_empty():
|
|
_round_trip(&"excavation", excavations.back()[&"job_id"], "地下掘削")
|
|
_main.call("_on_facility_selected", reception_id)
|
|
_expect(_runtime.save_to_path(_path).get(&"ok", false), "save selection through Runtime without explicit metadata")
|
|
var _audio_cleanup_transition: Dictionary = await _cleanup_fixture_owner(_main, "transition")
|
|
if not bool(_audio_cleanup_transition.get(&"passed", false)):
|
|
push_error("Ordinary fixture audio cleanup failed: %s" % str(_audio_cleanup_transition))
|
|
quit(96)
|
|
return
|
|
await process_frame
|
|
_expect(_runtime.get_signal_connection_list(&"checkpoint_presentation_requested").is_empty(), "destroyed UI leaves no presentation callback")
|
|
_expect(_runtime.save_to_path(_path + "_headless").get(&"ok", false), "Runtime still saves with no main scene")
|
|
var headless := SaveService.load_snapshot(_path + "_headless")
|
|
_expect(not headless.get(&"manifest", {}).has("interface_selection"), "UI-free save has no stale presentation")
|
|
await _mount()
|
|
_expect(_runtime.get_signal_connection_list(&"checkpoint_presentation_requested").size() == 1, "new main scene has exactly one presentation callback")
|
|
_expect(_runtime.load_from_path(_path).get(&"ok", false) and _main.get("_selected_entity_id") == reception_id, "new main scene receives direct-load selection")
|
|
# Rotation must preserve each candidate's own selection, rather than the newest UI state.
|
|
for selected: StringName in [reception_id, stair_id]:
|
|
_main.call("_on_facility_selected", selected)
|
|
_expect(_runtime.queue_checkpoint(_path + "_generations", {}, true).get(&"ok", false), "queue an autosave generation")
|
|
_clock.advance_minutes(1)
|
|
_expect(_runtime.load_recovery_candidate(_path + "_generations", &"autosave", 1).get(&"ok", false) and _main.get("_selected_entity_id") == reception_id, "older generation restores its reception selection")
|
|
_expect(_runtime.load_recovery_candidate(_path + "_generations", &"autosave", 0).get(&"ok", false) and _main.get("_selected_entity_id") == stair_id, "newest generation restores its staircase selection")
|
|
await _test_legacy()
|
|
_finish()
|
|
|
|
func _test_legacy() -> void:
|
|
# Materialize a genuine pre-policy world connection through the normal migration contract.
|
|
_clock.reset(1, 6, 0)
|
|
_state.reset()
|
|
_runtime.initialize(_clock, _state, 42531)
|
|
_clock.set_paused(true)
|
|
_runtime.world_service.set_walkable(Vector3i(0, 1, 0), true)
|
|
_expect(_runtime.rebuild_path_graph_from_world(), "old-world boundary fixture has two adjacent floors")
|
|
var old: Dictionary = _runtime.create_snapshot()
|
|
var data: Dictionary = old[&"systems"][&"runtime"]
|
|
data[&"runtime_schema_version"] = 7
|
|
data.erase(&"elevators")
|
|
data.erase(&"legacy_stairs")
|
|
preload("res://tests/fixtures/pre_upkeep_runtime.gd").strip_empty_upkeep(data)
|
|
data[&"path"][&"schema_version"] = 3
|
|
for field: StringName in [&"vertical_walk_blockers", &"vertical_walk_requires_connector", &"transit_actor_ids"]:
|
|
data[&"path"].erase(field)
|
|
_expect(_runtime.restore_snapshot(old), "genuine old world migrates into owned legacy stairs")
|
|
_main.get("world_preview").set_runtime(_runtime)
|
|
var records: Array = _main.get("world_preview").get_legacy_stair_records()
|
|
_expect(not records.is_empty(), "migrated legacy connection is selectable")
|
|
if not records.is_empty():
|
|
var id: StringName = records[0][&"id"]
|
|
_main.call("_on_legacy_stair_selected", id)
|
|
_round_trip(&"legacy_stairs", id, "既設の階段")
|
|
var capture := OS.get_environment("THE_TREE_SELECTION_LIFECYCLE_CAPTURE")
|
|
if not capture.is_empty():
|
|
for frame: int in 5:
|
|
await process_frame
|
|
await RenderingServer.frame_post_draw
|
|
_expect(root.get_texture().get_image().save_png(capture) == OK, "capture migrated staircase selection after actual load")
|
|
|
|
func _finish() -> void:
|
|
if is_instance_valid(_main):
|
|
var _audio_cleanup_final: Dictionary = await _cleanup_fixture_owner(_main, "finish")
|
|
if not bool(_audio_cleanup_final.get(&"passed", false)):
|
|
push_error("Ordinary fixture audio cleanup failed: %s" % str(_audio_cleanup_final))
|
|
quit(96)
|
|
return
|
|
for suffix: String in ["", "_headless", "_generations"]:
|
|
SaveService.delete_save(_path + suffix)
|
|
print("Selection lifecycle: %d checks, %d failures" % [_checks, _failures])
|
|
quit(1 if _failures else 0)
|
|
|
|
|
|
## QA-only control proof routing for sequential ordinary Main owners.
|
|
## The old02cd/6be99 helpers and original Clock/Runtime values stay exact.
|
|
func _cleanup_fixture_owner(owner: Node, phase: String) -> Dictionary:
|
|
var original_control: String = OS.get_environment("THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF")
|
|
if original_control.is_empty() or not original_control.is_absolute_path():
|
|
return {&"passed": false, &"reason": "fresh absolute owned control proof required"}
|
|
var phase_control: String = original_control.get_basename() + "-" + phase + ".json"
|
|
if FileAccess.file_exists(phase_control):
|
|
return {&"passed": false, &"reason": "ordinary owner control proof overwrite forbidden"}
|
|
OS.set_environment("THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF", phase_control)
|
|
var report: Dictionary = await FixtureCleanup.free_owner_and_wait(self, owner)
|
|
OS.set_environment("THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF", original_control)
|
|
return report
|