extends RefCounted ## Diagnostic setup, never natural-play or OS-input proof. const Cleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd") const Codec := preload("res://tests/fixtures/p08-02/mixed_world.gd") const MEMBERS := ["world.bin", "manifest.json", "thumbnail.png", "journal.log"] var checks := 0 var failures := 0 var tree: SceneTree var runtime: Variant var clock: Variant var main: Variant var directory: String var role: String var actions: Array = [] var results: Array = [] var serial := 0 var target := Vector3i(-8, 0, 0) var budget: Dictionary = {} var input_method := "handler" var observed_cell: Variant = null var save_serial := 0 var save_records: Array = [] var operation_results: Array[Dictionary] = [] const INITIAL_SAVE := "user://saves/p08_initial" func annotate(row: Dictionary) -> void: row["input_method"] = input_method func selected_cell(cell: Vector3i) -> void: observed_cell = cell func expect(ok: bool, message: String) -> void: checks += 1 if not ok: failures += 1 push_error("P08 placement: " + message) func json_file(name: String, data: Variant) -> void: var file := FileAccess.open(directory.path_join(name), FileAccess.WRITE) expect(file != null, "write " + name) if file != null: file.store_string(JSON.stringify(data, "\t") + "\n") file.close() func capture(label: String) -> String: return capture_model(label, runtime.create_snapshot()) func capture_model(label: String, snapshot: Dictionary) -> String: var name := role + "-" + label json_file(name + ".json", Codec.new().typed(snapshot)) var file := FileAccess.open(directory.path_join(name + ".var"), FileAccess.WRITE) if file != null: file.store_buffer(var_to_bytes(snapshot)) file.close() else: expect(false, "raw Variant " + name) return name func vector(cell: Vector3i) -> Array: return [cell.x, cell.y, cell.z] func affine(value: Transform2D) -> Array: return [value.x.x, value.x.y, value.y.x, value.y.y, value.origin.x, value.origin.y] func observe(result: CommandResult) -> void: results.append(result.to_dictionary()) func frames() -> void: await tree.process_frame await tree.process_frame func button(node: BaseButton) -> void: # Actual production button signal/connection, explicitly not native OS. expect(node != null and not node.disabled, "available production button") if node == null or node.disabled: return if node.toggle_mode: node.button_pressed = not node.button_pressed node.pressed.emit() await frames() func choose(id: StringName) -> void: if not main.palette_scroll.visible: await button(main.palette_toggle_button) var node: Button = main.build_buttons[id] await reveal(node) if not node.button_pressed: await button(node) expect(node.button_pressed and main._selected_definition_id == id, "chosen production definition") func click(cell: Vector3i) -> Dictionary: var view: WorldPreview = main.world_preview var local := view.cell_to_canvas(cell) var viewport := view.get_global_transform_with_canvas() * local var event := InputEventMouseButton.new() event.button_index = MOUSE_BUTTON_LEFT event.pressed = true event.position = local # This is a handler test through the real WorldPreview, not an OS receipt. observed_cell = null view._gui_input(event) await frames() return {"world_cell": vector(cell), "selected_cell": vector(observed_cell if observed_cell is Vector3i else view.canvas_to_cell(local)), "local": [local.x, local.y], "viewport": [viewport.x, viewport.y], "viewport_transform": affine(view.get_global_transform_with_canvas()), "map_position": [view.get_map_rect().position.x, view.get_map_rect().position.y], "cell_dimensions": [view.get_cell_rect(cell).size.x, view.get_cell_rect(cell).size.y], "bounds": runtime.world_service.get_bounds(), "zoom": view.zoom_level, "pan": [view.pan_offset.x, view.pan_offset.y]} func build(id: StringName, cell: Vector3i, label: String, expected: String = "") -> void: await choose(id) if label == "unsupported": cell = unsupported_cell() var report: Dictionary = main.world_preview.get_build_preview_report(cell) var before := capture(label + "-before") results.clear() var position := await click(cell) var status: String = main.build_status_label.text var after := capture(label + "-after") if expected.is_empty(): expect(not results.is_empty() and results.back().code == "OK", label + " accepted") elif expected == "STAIR_LANDING_MISSING": expect(not report.can_build and expected in report.error_codes and results.is_empty(), label + " preview refusal") expect(status == "配置不可: " + String(report.reasons[0].message), label + " preview refusal status") else: expect(expected in report.error_codes and results.is_empty(), label + " preview refusal") var row := {"id": label, "kind": "build", "definition": String(id), "expected": expected, "report": report, "before": before, "after": after, "position": position, "results": results.duplicate(true), "status": status, "input_method": "handler"} annotate(row) actions.append(row) if expected.is_empty(): await complete_build(id) row["completed"] = capture(label + "-completed") func unsupported_cell() -> Vector3i: return Vector3i(4, 4, 0) func demolish(cell: Vector3i, label: String, cancel := false, expected := "") -> void: if not main.palette_scroll.visible: await button(main.palette_toggle_button) if not main._demolition_mode: await button(main.demolition_button) var before := capture(label + "-before") results.clear() var position := await click(cell) var instance_id: String = String(main._demolition_target) expect(main.demolition_dialog.visible, "normal confirmation visible") if cancel: await button(main.demolition_dialog.get_cancel_button()) else: await button(main.demolition_dialog.get_ok_button()) await frames() if cancel: expect(results.is_empty(), "cancel submits no command") else: expect(not results.is_empty(), "confirmation submitted Runtime command") if not results.is_empty(): expect(results.back().code == ("OK" if expected.is_empty() else "VALIDATION_FAILED"), label + " result code") expect(String(results.back().details.get("reason", "")) == expected, label + " result reason") var row := {"id": label, "kind": "cancel" if cancel else "demolish", "expected": expected, "instance_id": instance_id, "position": position, "before": before, "after": capture(label + "-after"), "results": results.duplicate(true), "status": main.build_status_label.text, "input_method": input_method} annotate(row) actions.append(row) func camera() -> void: await button(main.pan_button) var view: WorldPreview = main.world_preview var before := capture("pan-before") var original := view.cell_to_canvas(target) var event := InputEventMouseButton.new() event.button_index = MOUSE_BUTTON_WHEEL_UP event.pressed = true event.factor = 1.0 event.position = view.size * 0.5 event.ctrl_pressed = false var zoom: float = view.zoom_level var original_pan: Vector2 = view.pan_offset await wheel(event, true) expect(view.zoom_level == zoom, "move-mode wheel pans without Ctrl") var moved_pan: Vector2 = view.pan_offset expect(moved_pan != original_pan, "move-mode wheel actually moves the camera") await button(main.pan_button) var steps: Array = [] for direction: int in [MOUSE_BUTTON_WHEEL_DOWN, MOUSE_BUTTON_WHEEL_UP]: for unused: int in range(9): event.button_index = direction await wheel(event, false) steps.append(view.zoom_level) expect(view.zoom_level == (0.5 if direction == MOUSE_BUTTON_WHEEL_DOWN else 4.0), "existing zoom clamp") # Use transformed pixels; camera state is not required to survive save/load. var position := await click(target) expect(position.world_cell == position.selected_cell, "camera world identity") expect(view.cell_to_canvas(target) != original, "pixels transform") var row := {"id": "pan_zoom", "kind": "camera", "before": before, "after": capture("pan-after"), "position": position, "original_local": [original.x, original.y], "steps": steps, "pan_before": [original_pan.x, original_pan.y], "pan_after": [moved_pan.x, moved_pan.y], "ctrl_pressed": false, "input_method": input_method} annotate(row) actions.append(row) func diagnostic_budget() -> void: var state: Variant = tree.root.get_node("GameState") var needed := {&"gold": 0, &"materials": 0} var costs: Array = [] for id: StringName in [&"reception", &"reception", &"guest_stairs", &"guest_stairs"]: var definition: FacilityDefinition = runtime.definition_registry.get_facility(id) costs.append({"definition": String(id), "cost": definition.construction_cost.duplicate(true)}) for key: StringName in needed: needed[key] += int(definition.construction_cost.get(key, 0)) var deltas := {} for key: StringName in needed: if needed[key] > state.get_resource(key): deltas[key] = needed[key] - state.get_resource(key) budget = {"setup": "diagnostic", "costs": costs, "needed": needed, "initial": state.resources.duplicate(true), "deltas": deltas} if not deltas.is_empty(): expect(runtime.submit_command(&"change_resources", {&"deltas": deltas}).is_success(), "definition-derived diagnostic budget") func writer() -> void: diagnostic_budget() await save_initial_fixture() await build(&"standard_room", Vector3i(4, 4, 0), "unsupported", "UNSUPPORTED") await build(&"reception", Vector3i(runtime.world_service.get_bounds()[&"max_x"], 0, 0), "out_of_bounds", "OUT_OF_BOUNDS") await build(&"reception", target, "supported_connected") await build(&"reception", target, "overlap", "OVERLAP") # Existing reception supports this upper cell, but provides no stair landing. await build(&"guest_stairs", Vector3i(-8, 2, 0), "missing_connection", "STAIR_LANDING_MISSING") await build(&"guest_stairs", Vector3i(4, 1, 0), "lower_stair") await build(&"guest_stairs", Vector3i(4, 2, 0), "upper_stair") await demolish(Vector3i(4, 1, 0), "refused_demolition", false, "STAIR_LANDING_IN_USE") await demolish(target, "cancel_demolition", true) await demolish(target, "demolition") await build(&"reception", target, "rebuild") await camera() capture("presave") await button(main.find_child("ManualSaveButton", true, false)) expect(main.build_status_label.text == "手動セーブを保存しました。", "normal save") capture("checkpoint") await archive_formal() var formal := inspect_formal() var presave: Dictionary = bytes_to_var(FileAccess.get_file_as_bytes(directory.path_join("writer-presave.var"))) expect(var_to_bytes(presave) == var_to_bytes(formal), "whole presave formal model") verify_operation(formal, operation_results.back(), &"SAVE_CHECKPOINT_SUCCEEDED") var members := {} for member: String in MEMBERS: members[member] = FileAccess.get_sha256(directory.path_join("formal/" + member)) json_file("save.json", {"members": members, "instance_id": String(runtime.world_service.get_occupant(target)), "cell": vector(target), "pid": OS.get_process_id()}) func reader(distinct: bool) -> void: var saved: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("save.json"))) if distinct: expect(saved.pid != OS.get_process_id(), "separate actual writer/reader PID") # Real load menu callback, no direct restore_snapshot or model mutation. var menu: MenuButton = main.find_child("SaveLoadMenu", true, false) await load_menu(menu) await frames() expect(main.build_status_label.text == "手動セーブを読み込みました。", "normal resume") capture("checkpoint") var formal := inspect_formal() var original := FileAccess.get_file_as_bytes(directory.path_join("writer-formal-model.var")) expect(original == var_to_bytes(formal), "whole formal resume model") verify_operation(formal, operation_results.back(), &"SAVE_RECOVERY_LOADED") expect(String(runtime.world_service.get_occupant(target)) == saved.instance_id, "same resumed instance/cell") for member: String in MEMBERS: expect(FileAccess.get_sha256(directory.path_join("formal/" + member)) == saved.members[member], "reader preserves formal member " + member) await build(&"reception", target, "resume_overlap", "OVERLAP") func run(p_tree: SceneTree, output: String, requested_role: String) -> Dictionary: # Handler entry is never an OS evidence route. Passive subclass overrides endpoints. tree = p_tree directory = output role = "reader" if requested_role == "reader" else "writer" expect(DirAccess.make_dir_recursive_absolute(directory) == OK, "diagnostic directory") runtime = tree.root.get_node("SimulationRuntime") clock = tree.root.get_node("GameClock") runtime.save_operation_completed.connect(record_save_operation) await setup() runtime.command_completed.connect(observe) main.world_preview.cell_clicked.connect(selected_cell) await frames() if role == "writer": await writer() else: await reader(true) json_file(role + "-actions.json", {"setup": "diagnostic", "input_method": input_method, "budget": budget, "actions": actions}) if requested_role == "standalone": role = "reader" actions = [] save_records = [] await reader(false) json_file(role + "-actions.json", {"setup": "diagnostic", "input_method": input_method, "actions": actions}) finish_observations() runtime.command_completed.disconnect(observe) runtime.save_operation_completed.disconnect(record_save_operation) var cleanup: Dictionary = await Cleanup.free_owner_and_wait(tree, main) expect(cleanup.get(&"passed", false), "ordinary Main/audio cleanup") json_file(role + "-process.json", {"pid": OS.get_process_id(), "engine": Engine.get_version_info(), "checks": checks, "failures": failures, "role": requested_role, "setup": "diagnostic"}) return {"checks": checks, "failures": failures} func reveal(node: Control) -> void: main.palette_scroll.ensure_control_visible(node) await frames() func wheel(event: InputEventMouseButton, _move_mode: bool) -> void: main.world_preview._gui_input(event) await frames() func complete_build(id: StringName) -> void: clock.advance_minutes(runtime.definition_registry.get_facility(id).build_minutes) await frames() func load_menu(menu: MenuButton) -> void: menu.get_popup().id_pressed.emit(0) func setup() -> void: clock.reset(1, 6, 0) tree.root.get_node("GameState").reset() runtime.initialize(clock, tree.root.get_node("GameState"), 42533) clock.set_paused(true) tree.root.size = Vector2i(1440, 810) main = load("res://scenes/main/main.tscn").instantiate() main.manual_save_path = "user://saves/manual_ui" if role == "reader": import_package(directory.path_join("formal"), String(main.manual_save_path)) tree.root.add_child(main) runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false}) func save_initial_fixture() -> void: # Explicit diagnostic input, not an OS action or natural-play save. var saved: Dictionary = SaveService.save_snapshot(INITIAL_SAVE, runtime.create_snapshot()) record_save_result("initial_save", INITIAL_SAVE, saved) expect(bool(saved.get("ok", false)), "diagnostic initial formal save") export_package(INITIAL_SAVE, "initial-formal") # Seed the private normal slot, so its later normal save produces a backup. import_package(ProjectSettings.globalize_path(INITIAL_SAVE), String(main.manual_save_path)) json_file("diagnostic-budget.json", budget) func archive_formal() -> void: export_package(String(main.manual_save_path), "formal") func inspect_formal() -> Dictionary: var stored := SaveService.inspect_snapshot(String(main.manual_save_path)) record_save_result("formal_inspect", String(main.manual_save_path), stored) expect(bool(stored.get(&"ok", false)), "read-only formal inspection") var formal: Dictionary = stored.get(&"snapshot", {}) capture_model("formal-model", formal) return formal func verify_operation(formal: Dictionary, result: Dictionary, event_type: StringName, prefix: String = "") -> void: var expected := formal.duplicate(true) var model: Dictionary = expected[&"systems"][&"runtime"] var counters: Dictionary = model[&"id_generator"]["counters"] var next: int = int(counters.get("domain_event", 0)) + 1 counters["domain_event"] = next var event_id := "domain_event.%012d" % next var payload := { &"ok": bool(result.get(&"ok", false)), &"code": StringName(result.get(&"code", &"UNKNOWN")), &"message": str(result.get(&"message", "")), &"operation": StringName(result.get(&"operation", &"unknown")), &"save_path": str(result.get(&"save_path", result.get(&"path", ""))), &"reason": StringName(result.get(&"reason", &"")), &"request_id": int(result.get(&"request_id", 0)), &"checkpoint_absolute_minute": int(result.get(&"checkpoint_absolute_minute", formal[&"clock"][&"absolute_minute"])), &"duration_us": int(result.get(&"duration_us", 0)), &"runtime_duration_us": int(result.get(&"runtime_duration_us", 0)), } if result.get(&"selected") is Dictionary: var selected: Dictionary = result[&"selected"] payload[&"selected_candidate_id"] = StringName(selected.get(&"id", &"")) payload[&"selected_candidate_kind"] = StringName(selected.get(&"kind", &"")) payload[&"selected_generation"] = int(selected.get(&"generation", -1)) if result.get(&"manifest") is Dictionary: var manifest: Dictionary = result[&"manifest"] payload[&"save_id"] = str(manifest.get("save_id", "")) payload[&"saved_absolute_minute"] = int(manifest.get("absolute_minute", -1)) var stream: Dictionary = model[&"domain_events"] stream["published_ids"].append(event_id) stream["published_ids"].sort() var notification := SimulationDomainEvent.new(StringName(event_id), event_type, int(formal[&"clock"][&"absolute_minute"]), &"save_service", [], payload, 0) stream["recent_events"].append(notification.to_dictionary()) if stream["recent_events"].size() > DomainEventStream.RECENT_EVENT_LIMIT: stream["recent_events"].pop_front() capture_model(prefix + "expected-operation", expected) capture(prefix + "operation-actual") var actual: Dictionary = bytes_to_var(FileAccess.get_file_as_bytes(directory.path_join(role + "-" + prefix + "checkpoint.var"))) json_file(role + "-" + prefix + "operation-comparison.json", { "pid": OS.get_process_id(), "delta_kind": "source_defined_notification", "formal_model": role + "-" + prefix + "formal-model", "actual_model": role + "-" + prefix + "checkpoint", "expected_model": role + "-" + prefix + "expected-operation", "returned_result": Codec.new().typed(result), "saved_recent_count": formal[&"systems"][&"runtime"][&"domain_events"]["recent_events"].size(), "expected_recent_count": stream["recent_events"].size(), "actual_recent_count": actual[&"systems"][&"runtime"][&"domain_events"]["recent_events"].size(), "whole_raw_equal": var_to_bytes(expected) == var_to_bytes(actual), }) # Complete raw actual/expected snapshots remain available on unknown drift. expect(var_to_bytes(expected) == var_to_bytes(actual), "whole source-defined save operation model; see " + role + "-" + prefix + "operation-comparison.json") expect(var_to_bytes(actual) == var_to_bytes(runtime.create_snapshot()), "read-only inspection preserves live model") func record_save_operation(result: Dictionary) -> void: operation_results.append(result.duplicate(true)) record_save_result("runtime_operation", String(main.manual_save_path), result) func record_save_result(operation: String, path: String, result: Dictionary) -> void: save_serial += 1 var stem := "%s-save-result-%04d" % [role, save_serial] var raw := FileAccess.open(directory.path_join(stem + ".var"), FileAccess.WRITE) expect(raw != null, "raw save result") if raw != null: raw.store_buffer(var_to_bytes(result)) raw.close() json_file(stem + ".json", {"pid": OS.get_process_id(), "operation": operation, "requested_path": path, "absolute_path": ProjectSettings.globalize_path(path), "user_data_dir": OS.get_user_data_dir(), "result": Codec.new().typed(result), "raw_sha256": FileAccess.get_sha256(directory.path_join(stem + ".var"))}) save_records.append(stem) json_file(role + "-save-results.json", {"pid": OS.get_process_id(), "records": save_records}) func import_package(source: String, destination: String) -> void: var absolute := ProjectSettings.globalize_path(destination) expect(DirAccess.make_dir_recursive_absolute(absolute) == OK, "private save directory") for member: String in MEMBERS: var origin := source.path_join(member) var copy := absolute.path_join(member) expect(DirAccess.copy_absolute(origin, copy) == OK, "private save input " + member) expect(FileAccess.get_sha256(origin) == FileAccess.get_sha256(copy), "private save input SHA " + member) func export_package(path: String, name: String) -> void: var absolute := ProjectSettings.globalize_path(path) var pins := {} for suffix: String in ["", ".bak"]: var source := absolute + suffix var present := DirAccess.dir_exists_absolute(source) pins[name + suffix] = {"source_path": source, "present": present, "members": {}} if not present: expect(not suffix.is_empty(), "formal package exists") continue var destination := directory.path_join(name + suffix) expect(DirAccess.make_dir_recursive_absolute(destination) == OK, "export directory") for member: String in MEMBERS: var origin := source.path_join(member) var copy := destination.path_join(member) expect(DirAccess.copy_absolute(origin, copy) == OK, "export " + member) var digest := FileAccess.get_sha256(origin) expect(not digest.is_empty() and digest == FileAccess.get_sha256(copy), "export SHA " + member) pins[name + suffix]["members"][member] = {"sha256": digest, "bytes": FileAccess.get_file_as_bytes(copy).size()} # Slot metadata lives in the existing manifest; copy it verbatim. var metadata := absolute.path_join("manifest.json") var metadata_copy := directory.path_join(name + "-slot-metadata.json") expect(DirAccess.copy_absolute(metadata, metadata_copy) == OK, "slot metadata export") json_file(name + "-export.json", {"pid": OS.get_process_id(), "requested_path": path, "absolute_path": absolute, "user_data_dir": OS.get_user_data_dir(), "packages": pins, "slot_metadata": {"file": name + "-slot-metadata.json", "sha256": FileAccess.get_sha256(metadata_copy)}}) func finish_observations() -> void: pass