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.
209 lines
12 KiB
GDScript
209 lines
12 KiB
GDScript
extends SceneTree
|
|
|
|
const FixtureCleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd")
|
|
var _finishing := false
|
|
|
|
var _checks := 0
|
|
var _failures := 0
|
|
var _runtime: Variant
|
|
var _clock: Variant
|
|
var _main: Control
|
|
var _staff: StaffManagementPanel
|
|
var _guest: GuestObservationPanel
|
|
var _path := "user://tests/actor_selection_%d/slot" % OS.get_process_id()
|
|
var _slots: Array[String] = []
|
|
|
|
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
|
|
_staff = _main.find_child("StaffManagementPanel", true, false)
|
|
_guest = null
|
|
|
|
func _choose(staff_id: StringName, guest_id: StringName, opened := true) -> void:
|
|
_main.call("_activate_management_mode", &"staff")
|
|
_staff.refresh()
|
|
_staff.roster_buttons[staff_id].pressed.emit()
|
|
_main.get("guest_count_label").pressed.emit()
|
|
_guest = _main.get("guest_observer").find_child("GuestObservationPanel", true, false)
|
|
for i: int in _guest.guests.item_count:
|
|
if _guest.guests.get_item_metadata(i) == guest_id:
|
|
_guest.guests.select(i)
|
|
_guest.guests.item_selected.emit(i)
|
|
if not opened:
|
|
_main.call("_set_staff_management_panel_open", false)
|
|
_main.get("guest_observer").hide()
|
|
|
|
func _check_selected(staff_id: StringName, guest_id: StringName, opened: bool, context: String) -> void:
|
|
var observer: AcceptDialog = _main.get("guest_observer")
|
|
_expect(observer != null, context + " creates a real guest inspector when required")
|
|
if observer == null:
|
|
return
|
|
_guest = observer.find_child("GuestObservationPanel", true, false)
|
|
_expect(_staff.selected_staff_id == staff_id, context + " restores saved staff instead of first roster entry")
|
|
_expect(_guest.selected_guest_id == guest_id and _guest.guests.get_item_metadata(_guest.guests.selected) == guest_id, context + " restores saved guest and dropdown")
|
|
_expect(_staff.visible == opened and _main.get("guest_observer").visible == opened, context + " restores inspector visibility")
|
|
var amount: int = _runtime.guest_service.get_guest(guest_id).wallet_gold
|
|
_expect("%dG" % amount in _guest.details.text, context + " guest details show the actual restored wallet")
|
|
var name := "バルド" if staff_id == &"staff_bardo" else "アカネ"
|
|
_expect(_runtime.staff_service.has_staff(staff_id) and name in _staff.staff_detail_label.text, context + " staff detail name matches the restored employee")
|
|
_expect(_guest.bars[&"hunger"].value == _runtime.guest_service.get_guest(guest_id).needs[&"hunger"], context + " need bar matches the independently saved guest")
|
|
|
|
func _gameplay_snapshot() -> Dictionary:
|
|
var snapshot: Dictionary = _runtime.create_snapshot()
|
|
# Save/load observations contain measured duration and intentionally differ.
|
|
snapshot[&"systems"][&"runtime"].erase(&"domain_events")
|
|
return snapshot
|
|
|
|
func _gameplay_matches(saved: Dictionary, context: String, observation_count := 0) -> bool:
|
|
var expected := saved.duplicate(true)
|
|
# Runtime reports every load attempt using one new diagnostic event ID.
|
|
# Assert that exact increment; retain every other allocator and subsystem.
|
|
var counters: Dictionary = expected[&"systems"][&"runtime"][&"id_generator"][&"counters"]
|
|
counters["domain_event"] = int(counters.get("domain_event", 0)) + observation_count
|
|
var actual := _gameplay_snapshot()
|
|
var equal := SimulationDigest.compute(expected) == SimulationDigest.compute(actual)
|
|
if not equal:
|
|
var file := FileAccess.open("res://artifacts/actor-selection-2026-09-30/" + context + "-gameplay-diff.json", FileAccess.WRITE)
|
|
file.store_string(JSON.stringify({"expected": expected, "actual": actual}, "\t"))
|
|
return equal
|
|
|
|
func _write_fixture(suffix: String, selection: Variant) -> String:
|
|
var path := _path + suffix
|
|
_slots.append(path)
|
|
var metadata := {} if selection == null else {&"interface_selection": selection}
|
|
_expect(SaveService.save_snapshot(path, _runtime.create_snapshot(), metadata).get(&"ok", false), "write gameplay-valid optional presentation fixture " + suffix)
|
|
return path
|
|
|
|
func _run() -> void:
|
|
root.gui_embed_subwindows = true
|
|
root.size = Vector2i(1440, 810)
|
|
_runtime = root.get_node("SimulationRuntime")
|
|
_clock = root.get_node("GameClock")
|
|
_clock.reset(1, 6, 0)
|
|
root.get_node("GameState").reset()
|
|
_runtime.initialize(_clock, root.get_node("GameState"), 42532)
|
|
_clock.set_paused(true)
|
|
await _mount()
|
|
_main.get("staff_candidate_toggle_button").pressed.emit()
|
|
for id: StringName in [&"staff_akane", &"staff_bardo"]:
|
|
_main.get("staff_candidate_buttons")[id].pressed.emit()
|
|
_expect(_runtime.staff_service.has_staff(&"staff_akane") and _runtime.staff_service.has_staff(&"staff_bardo"), "ordinary candidate buttons hire both real employees")
|
|
# Controlled arrival fixtures isolate observer persistence, not natural arrivals.
|
|
var definition: GuestDefinition = _runtime.guest_service.get_definitions()[0]
|
|
for row: Array in [[&"selection_guest_a", 510, 17], [&"selection_guest_b", 910, 83]]:
|
|
_expect(_runtime.submit_command(&"spawn_guest", {&"guest_id": row[0], &"guest_type_id": definition.guest_type_id, &"gold": row[1], &"options": {&"planned_exit_at": _clock.absolute_minute + 1000, &"needs": {&"hunger": row[2]}}}).is_success(), "spawn an actual saved visitor " + String(row[0]))
|
|
_choose(&"staff_bardo", &"selection_guest_b")
|
|
var captured_before: Dictionary = _runtime.create_snapshot()
|
|
_main.call("_on_checkpoint_presentation_requested", {})
|
|
_expect(captured_before == _runtime.create_snapshot(), "collecting actor presentation does not mutate any Runtime subsystem")
|
|
_main.call("_save_manual")
|
|
var before := _gameplay_snapshot()
|
|
_slots.append(_path)
|
|
var package := SaveService.load_snapshot(_path)
|
|
_expect(package.get(&"ok", false), "normal manual save creates a gameplay package")
|
|
var selection: Dictionary = package.get(&"manifest", {}).get("interface_selection", {})
|
|
_expect(selection.get("staff_id") == "staff_bardo" and selection.get("guest_id") == "selection_guest_b", "normal save captures both independently chosen actor IDs")
|
|
_choose(&"staff_akane", &"selection_guest_a", false)
|
|
_main.call("_load_manual", true)
|
|
_expect(_main.get("build_status_label").text == "手動セーブを読み込みました。", "normal strict load accepts the complete actor gameplay package")
|
|
_check_selected(&"staff_bardo", &"selection_guest_b", true, "manual strict load")
|
|
_expect(_gameplay_matches(before, "manual"), "actor restoration does not change loaded gameplay")
|
|
var capture := OS.get_environment("THE_TREE_ACTOR_SELECTION_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 loaded guest inspector over saved staff panel")
|
|
_main.get("guest_observer").hide()
|
|
_main.call("_set_staff_management_panel_open", false)
|
|
var hidden_path := _path + "_hidden"
|
|
_slots.append(hidden_path)
|
|
_expect(_runtime.save_to_path(hidden_path).get(&"ok", false), "direct Runtime save collects hidden inspector selections")
|
|
var remount_cleanup: Dictionary = await _cleanup_fixture_owner(_main, "before-remount")
|
|
if not bool(remount_cleanup.get(&"passed", false)):
|
|
_failures += 1
|
|
push_error("ordinary Main remount audio cleanup failed")
|
|
await _mount()
|
|
_expect(_main.get("guest_observer") == null, "new main has no pre-existing guest popup")
|
|
_expect(_runtime.load_from_path(hidden_path).get(&"ok", false), "new main accepts hidden actor selection package")
|
|
_check_selected(&"staff_bardo", &"selection_guest_b", false, "fresh main hidden load")
|
|
_choose(&"staff_bardo", &"selection_guest_b")
|
|
var intact := _gameplay_snapshot()
|
|
_expect(not _runtime.load_from_path(_path + "_missing").get(&"ok", true), "missing package is rejected")
|
|
_check_selected(&"staff_bardo", &"selection_guest_b", true, "failed load")
|
|
_expect(_gameplay_matches(intact, "failed", 1), "failed load preserves gameplay")
|
|
var generations := _path + "_generations"
|
|
_slots.append(generations)
|
|
for pair: Array in [[&"staff_bardo", &"selection_guest_b"], [&"staff_akane", &"selection_guest_a"]]:
|
|
_choose(pair[0], pair[1])
|
|
_expect(_runtime.queue_checkpoint(generations, {}, true).get(&"ok", false), "queue actual rotating checkpoint")
|
|
_clock.advance_minutes(1)
|
|
_expect(_runtime.load_recovery_candidate(generations, &"autosave", 1).get(&"ok", false), "load older actor checkpoint generation")
|
|
_check_selected(&"staff_bardo", &"selection_guest_b", true, "older recovery candidate")
|
|
_expect(_runtime.load_recovery_candidate(generations, &"autosave", 0).get(&"ok", false), "load newest actor checkpoint generation")
|
|
_check_selected(&"staff_akane", &"selection_guest_a", true, "newest recovery candidate")
|
|
# Invalid primary map metadata must not discard valid actor presentation.
|
|
var independent := {&"version": 1, &"kind": [], &"id": {}, &"staff_id": "staff_bardo", &"staff_open": true, &"guest_id": "selection_guest_b", &"guest_open": true}
|
|
var independent_path := _write_fixture("_independent", independent)
|
|
_expect(_runtime.load_from_path(independent_path).get(&"ok", false), "invalid map presentation does not invalidate gameplay")
|
|
_check_selected(&"staff_bardo", &"selection_guest_b", true, "independent actor metadata")
|
|
for entry: Array in [
|
|
["_missing_ids", {&"version": 1, &"staff_id": "nonexistent_staff", &"staff_open": true, &"guest_id": "nonexistent_guest", &"guest_open": true}],
|
|
["_wrong_types", {&"version": 1, &"staff_id": [], &"staff_open": true, &"guest_id": 17, &"guest_open": true}],
|
|
["_long_ids", {&"version": 1, &"staff_id": "a".repeat(257), &"staff_open": true, &"guest_id": "b".repeat(257), &"guest_open": true}],
|
|
["_wrong_version", {&"version": 9, &"staff_id": "staff_bardo", &"staff_open": true, &"guest_id": "selection_guest_b", &"guest_open": true}],
|
|
["_legacy", {&"version": 1, &"kind": "", &"id": ""}],
|
|
["_none", null],
|
|
]:
|
|
_choose(&"staff_bardo", &"selection_guest_b")
|
|
var path := _write_fixture(entry[0], entry[1])
|
|
var expected := _gameplay_snapshot()
|
|
_expect(_runtime.load_from_path(path).get(&"ok", false), "optional invalid or old metadata still loads " + entry[0])
|
|
_expect(not _staff.visible and not _main.get("guest_observer").visible, "invalid or absent actor metadata does not reopen inspectors " + entry[0])
|
|
_expect(_staff.selected_staff_id != &"nonexistent_staff" and _guest.selected_guest_id != &"nonexistent_guest", "missing actor IDs never become active selections " + entry[0])
|
|
_expect(_gameplay_matches(expected, entry[0], 1), "presentation fallback preserves gameplay " + entry[0])
|
|
var wrong_flags := {&"version": 1, &"staff_id": "staff_bardo", &"staff_open": "true", &"guest_id": "selection_guest_b", &"guest_open": 1}
|
|
_expect(_runtime.load_from_path(_write_fixture("_flags", wrong_flags)).get(&"ok", false), "wrong visibility types do not block valid actor selection")
|
|
_check_selected(&"staff_bardo", &"selection_guest_b", false, "strict visibility flags")
|
|
_finish()
|
|
|
|
func _finish() -> void:
|
|
if _finishing:
|
|
return
|
|
_finishing = true
|
|
if is_instance_valid(_main):
|
|
var final_cleanup: Dictionary = await _cleanup_fixture_owner(_main, "final")
|
|
if not bool(final_cleanup.get(&"passed", false)):
|
|
_failures += 1
|
|
push_error("ordinary Main final audio cleanup failed")
|
|
for path: String in _slots:
|
|
SaveService.delete_save(path)
|
|
print("Actor selection: %d checks, %d failures" % [_checks, _failures])
|
|
quit(1 if _failures else 0)
|
|
|
|
|
|
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
|