extends "res://tests/fixtures/p08-03/placement.gd" ## Passive endpoint implementation. All gameplay/input is performed by Root CUA. ## Shared parent contains the handler endpoints, but every such endpoint below ## is overridden; this class never emits UI signals or invokes product handlers. var receipts: Array = [] var terminated := false var terminal_record: Dictionary = {} var terminal_cause_recorded := false var reveal_rows: Array = [] var all_requests: Array = [] var input_observer: Node var last_dispatch: Dictionary = {} class InputObserver extends Node: var events: Array = [] var endpoint: RefCounted = null func _input(event: InputEvent) -> void: if event is InputEventMouseButton or event is InputEventKey: var row := {"ticks_usec": Time.get_ticks_usec(), "text": event.as_text(), "pressed": event.is_pressed()} if event is InputEventMouseButton: row["button"] = event.button_index row["position"] = [event.position.x, event.position.y] row["ctrl"] = event.ctrl_pressed if endpoint != null and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: row["dispatch"] = endpoint.dispatch_position(event.position) events.append(row) func expect(ok: bool, message: String) -> void: if terminated and ok: return if terminated and terminal_cause_recorded: if not ok: terminal_record["suppressed_after_terminal"] += 1 json_file(role + "-terminal.json", terminal_record) return if terminated and not ok: terminal_cause_recorded = true super.expect(ok, message) func terminate(stem: String, reason: String) -> void: terminated = true terminal_record = {"pid": OS.get_process_id(), "role": role, "sequence": serial, "stem": stem, "reason": reason, "suppressed_after_terminal": 0} json_file(role + "-terminal.json", terminal_record) expect(false, reason) tree.quit(1) func request(action: Dictionary) -> void: if terminated: return serial += 1 var stem := "%s-request-%04d" % [role, serial] var folder := directory.path_join(stem) DirAccess.make_dir_recursive_absolute(folder) var window: Window = main.get_window() action.merge({"sequence": serial, "target_pid": OS.get_process_id(), "role": role, "state": "waiting_for_root_cua", "window_title": window.title, "window_position": [window.position.x, window.position.y], "window_size": [window.size.x, window.size.y], "screen_scale": DisplayServer.screen_get_scale(window.current_screen), "content_scale_factor": window.content_scale_factor, "setup": "diagnostic"}) json_file(stem + "/request.json", action) var path := folder.path_join("ack.json") var first_event: int = input_observer.events.size() var deadline := Time.get_ticks_msec() + 300000 print("P08_ROOT_CUA_WAIT " + folder.path_join("request.json")) while not FileAccess.file_exists(path) and Time.get_ticks_msec() < deadline: await tree.process_frame if not FileAccess.file_exists(path): terminate(stem, "Root CUA acknowledgement timeout") return var ack: Variant = JSON.parse_string(FileAccess.get_file_as_string(path)) if not ack is Dictionary or ack.get("target_pid") != OS.get_process_id() or ack.get("sequence") != serial or ack.get("request_sha256") != FileAccess.get_sha256(folder.path_join("request.json")): terminate(stem, "Root CUA acknowledgement identity/hash") return await frames() for event: Dictionary in input_observer.events.slice(first_event): if event.has("dispatch"): last_dispatch = event["dispatch"] json_file(stem + "/observed.json", {"pid": OS.get_process_id(), "sequence": serial, "observer_endpoint": "null_modal_receipts_only" if input_observer.endpoint == null else "passive_read_only_dispatch", "events": input_observer.events.slice(first_event), "status": main.build_status_label.text}) receipts.append("os-pair/" + stem + "/ack.json") all_requests.append("os-pair/" + stem + "/ack.json") func annotate(row: Dictionary) -> void: row["reveal"] = reveal_rows.duplicate(true) reveal_rows.clear() row["input_method"] = "native_os" row["requests"] = receipts.duplicate() receipts.clear() func point(node: Control) -> Dictionary: var viewport: Vector2 = node.get_global_transform_with_canvas() * (node.size * 0.5) var screen: Vector2 = node.get_screen_transform() * (node.size * 0.5) return {"node": String(node.get_path()), "viewport": [viewport.x, viewport.y], "screen": [screen.x, screen.y], "window_id": node.get_window().get_window_id(), "viewport_to_screen": affine(node.get_screen_transform() * node.get_global_transform_with_canvas().affine_inverse())} func unsupported_cell() -> Vector3i: # Read-only selection: retain the production predicate and avoid the guide. var view: WorldPreview = main.world_preview var bounds: Dictionary = runtime.world_service.get_bounds() for y: int in range(4, int(bounds[&"max_y"]) + 1): for x: int in range(int(bounds[&"max_x"]) - 1, int(bounds[&"min_x"]) - 1, -1): var cell := Vector3i(x, y, 0) var local := view.cell_to_canvas(cell) var viewport := view.get_global_transform_with_canvas() * local if not Rect2(Vector2.ZERO, view.size).has_point(local): continue if main.opening_guide.visible and main.opening_guide.get_global_rect().has_point(viewport): continue if "UNSUPPORTED" in view.get_build_preview_report(cell).get("error_codes", []): return cell expect(false, "visible unsupported target outside opening guide") return Vector3i(4, 4, 0) func setup() -> void: input_method = "native_os" expect(DisplayServer.get_name() != "headless", "OS recorder requires a real display") main = load("res://scenes/main/main.tscn").instantiate() tree.root.add_child(main) tree.root.title = "The Tree P08-03 " + role + " PID " + str(OS.get_process_id()) input_observer = InputObserver.new() input_observer.endpoint = self tree.root.add_child(input_observer) await frames() json_file(role + "-profile.json", {"pid": OS.get_process_id(), "user_data_dir": OS.get_user_data_dir(), "engine": Engine.get_version_info(), "display": DisplayServer.get_name()}) if role == "writer": await load_menu(main.find_child("SaveLoadMenu", true, false)) var stored: Dictionary = SaveService.inspect_snapshot(ProjectSettings.globalize_path(String(main.manual_save_path))) record_save_result("initial_inspect", String(main.manual_save_path), stored) expect(bool(stored.get("ok", false)), "read-only initial formal inspection") var formal: Dictionary = stored.get("snapshot", {}) capture_model("bootstrap-formal-model", formal) capture("bootstrap-checkpoint") expect(not operation_results.is_empty(), "bootstrap returned load Dictionary") if not operation_results.is_empty(): var returned: Dictionary = operation_results.back() capture_model("bootstrap-returned-result", returned) verify_operation(formal, returned, &"SAVE_RECOVERY_LOADED", "bootstrap-") expect(clock.paused, "diagnostic formal fixture starts paused") func diagnostic_budget() -> void: budget = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("diagnostic-budget.json"))) func save_initial_fixture() -> void: # The controller copied the handler's SHA-pinned formal diagnostic fixture. # Root loaded it using the normal menu. No model/setup mutation here. pass func rect_values(rect: Rect2) -> Array: return [rect.position.x, rect.position.y, rect.size.x, rect.size.y] func control_rect(node: Control, screen: bool = false) -> Rect2: var transform := node.get_screen_transform() if screen else node.get_global_transform_with_canvas() return transform * Rect2(Vector2.ZERO, node.size) func reveal_geometry(node: Control) -> Dictionary: var bar: HScrollBar = main.palette_scroll.get_h_scroll_bar() var bar_rect := control_rect(bar) var span := bar.max_value - bar.min_value var thumb := bar_rect if span > 0: thumb.size.x = bar_rect.size.x * bar.page / span thumb.position.x += bar_rect.size.x * (bar.value - bar.min_value) / span return {"target_rect": rect_values(control_rect(node)), "target_screen_rect": rect_values(control_rect(node, true)), "palette_rect": rect_values(main.palette_scroll.get_global_rect()), "bar": {"rect": rect_values(bar_rect), "visible": bar.is_visible_in_tree(), "min_value": bar.min_value, "max_value": bar.max_value, "page": bar.page, "value": bar.value}, "thumb_rect": rect_values(thumb), "viewport_to_screen": point(node)["viewport_to_screen"]} func reveal(node: Control) -> void: if terminated: return var palette: Rect2 = main.palette_scroll.get_global_rect() var target_rect := control_rect(node) if palette.encloses(target_rect): reveal_rows.append({"node": String(node.get_path()), "direction": "none"}) return var direction := "right" if target_rect.end.x > palette.end.x else "left" var before: int = main.palette_scroll.scroll_horizontal var action := reveal_geometry(node) action.merge({"action": "reveal_palette", "node": String(node.get_path()), "direction": direction, "scroll_before": before, "instructions": "Use the normal horizontal scrollbar to reveal this palette button without activating it."}) await request(action) if terminated: return await frames() var after := reveal_geometry(node) var visible_after: bool = main.palette_scroll.get_global_rect().encloses(control_rect(node)) after.merge({"scroll_after": main.palette_scroll.scroll_horizontal, "visible_after": visible_after}) var stem := "%s-request-%04d" % [role, serial] json_file(stem + "/reveal.json", after) reveal_rows.append(action) var moved: bool = main.palette_scroll.scroll_horizontal > before if direction == "right" else main.palette_scroll.scroll_horizontal < before if not visible_after or not moved: terminate(stem, "palette target not visible after OS reveal") func button(node: BaseButton) -> void: if terminated: return expect(node != null and not node.disabled, "available production button") if node == null or node.disabled: return # Native modal windows own a separate input viewport. var old_observer: Node = input_observer if node.get_window() != main.get_window(): input_observer = InputObserver.new() input_observer.endpoint = null # Modal receipts only; never dispatch world cells. node.get_window().add_child(input_observer) var action := point(node) action.merge({"action": "button", "instructions": "Click the named button through CUA; capture before/after screenshots."}) await request(action) if input_observer != old_observer: input_observer.queue_free() input_observer = old_observer func click(cell: Vector3i) -> Dictionary: if terminated: return {} var view: WorldPreview = main.world_preview for attempt: int in range(8): if Rect2(Vector2.ZERO, view.size).has_point(view.cell_to_canvas(cell)): break var framing := point(view) framing.merge({"action": "framing", "world_cell": vector(cell), "instructions": "Use ordinary zoom-out at the preview center until the requested world cell is visible; coordinates will be recomputed after ack."}) await request(framing) if terminated: return {} expect(Rect2(Vector2.ZERO, view.size).has_point(view.cell_to_canvas(cell)), "OS target visible after normal framing") var local := view.cell_to_canvas(cell) var viewport := view.get_global_transform_with_canvas() * local var screen := view.get_screen_transform() * local var mapping := affine(view.get_screen_transform() * view.get_global_transform_with_canvas().affine_inverse()) var guide_rect: Rect2 = main.opening_guide.get_global_rect() observed_cell = null last_dispatch = {} await request({"action": "cell", "world_cell": vector(cell), "viewport": [viewport.x, viewport.y], "screen": [screen.x, screen.y], "viewport_to_screen": mapping, "guide_rect": [guide_rect.position.x, guide_rect.position.y, guide_rect.size.x, guide_rect.size.y], "guide_visible": main.opening_guide.visible, "instructions": "Click screen coordinates derived from viewport_to_screen (includes canvas stretch and window translation). Retain actual CUA coordinates and screenshots. If clipped or intercepted, recollect; never substitute fixed pixels."}) if terminated: return {} var selected: Array = last_dispatch.get("world_cell", []) expect(selected == vector(cell), "observed OS selected cell") var occupant: StringName = runtime.world_service.get_occupant(cell) var selection_required: bool = view.build_definition == null and not view.excavation_mode and not view.demolition_mode and not occupant.is_empty() if selection_required: expect(view.selected_entity_id == occupant, "actual OS facility selection") var getter: Variant = view.canvas_to_cell(view.get_local_mouse_position()) return {"world_cell": vector(cell), "selected_cell": selected, "dispatch": last_dispatch.duplicate(true), "selection": {"required": selection_required, "entity_id": String(view.selected_entity_id), "occupant_id": String(occupant)}, "diagnostics": {"getter_cell": vector(getter) if getter is Vector3i else []}, "local": [local.x, local.y], "viewport": [viewport.x, viewport.y], "viewport_transform": affine(view.get_global_transform_with_canvas()), "screen": [screen.x, screen.y], "screen_transform": affine(view.get_screen_transform()), "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 dispatch_position(position: Vector2) -> Dictionary: var view: WorldPreview = main.world_preview var local := view.get_global_transform_with_canvas().affine_inverse() * position var cell: Variant = view.canvas_to_cell(local) return {"world_cell": vector(cell) if cell is Vector3i else [], "entity_id_before": String(view.selected_entity_id), "occupant_id_before": String(runtime.world_service.get_occupant(cell)) if cell is Vector3i else "", "local": [local.x, local.y], "viewport": [position.x, position.y], "viewport_transform": affine(view.get_global_transform_with_canvas()), "screen_transform": affine(view.get_screen_transform()), "screen": vector2(view.get_screen_transform() * local), "bounds": runtime.world_service.get_bounds(), "map_position": vector2(view.get_map_rect().position), "cell_dimensions": vector2(view.get_cell_rect(Vector3i.ZERO).size), "zoom": view.zoom_level, "pan": vector2(view.pan_offset)} func vector2(value: Vector2) -> Array: return [value.x, value.y] func wheel(event: InputEventMouseButton, move_mode: bool) -> void: if terminated: return var action := point(main.world_preview) action.merge({"action": "wheel", "direction": "up" if event.button_index == MOUSE_BUTTON_WHEEL_UP else "down", "ctrl": false, "move_mode": move_mode, "instructions": "Send one ordinary wheel step at the preview center without Ctrl; retain the CUA receipt."}) await request(action) func complete_build(id: StringName) -> void: if terminated: return await request({"action": "complete_construction", "definition": String(id), "minutes": runtime.definition_registry.get_facility(id).build_minutes, "instructions": "Use normal speed/play controls, wait for construction completion, then pause. No injected clock or model changes."}) expect(clock.paused, "Root paused after ordinary construction progression") func load_menu(menu: MenuButton) -> void: if terminated: return var action := point(menu) action.merge({"action": "load_menu", "item_id": 0, "instructions": "Open this normal menu and choose manual load (item 0)."}) await request(action) func archive_formal() -> void: export_package(String(main.manual_save_path), "formal") func finish_observations() -> void: json_file(role + "-requests.json", {"pid": OS.get_process_id(), "requests": all_requests})