extends "res://tests/fixtures/p23-03/observation.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 = {} var observed_categories: Dictionary = {} func observe_progress() -> void: if role != "writer" or terminated: return var wanted: Dictionary = {} var report: Dictionary = main.world_preview.get_life_observation_report() for room: Dictionary in report.get(&"rooms", {}).values(): if int(room.get(&"count", 0)) > 0: wanted["room-users"] = true for badge: Dictionary in report.get(&"landing_badges", []): if int(badge.get(&"overflow", 0)) > 0: wanted["queue-overflow"] = true for cabin: Dictionary in report.get(&"cabins", []): if not cabin[&"passengers"].is_empty(): wanted["EV-passengers"] = true var elevators: Dictionary = runtime.elevators.get_report() for shaft: Dictionary in elevators.get(&"shafts", []): if not shaft.get(&"boarded", []).is_empty(): wanted["EV-passengers"] = true for trip: Dictionary in elevators.get(&"trips", []): if trip.get(&"state", &"") == &"QUEUED": wanted["queue-present"] = true for member: StaffRuntime in runtime.staff_service.get_staff_members(): if not member.carried_inventory_id.is_empty(): wanted["carried-goods"] = true if not report.get(&"resting_staff", {}).is_empty(): wanted["rest-position"] = true for category: String in wanted: if observed_categories.has(category): continue observed_categories[category] = true await layer_observations("actual-" + category) if category == "EV-passengers": await motion_observations() func motion_observations() -> void: await button(main.front_button) await button(main.pause_button) expect(not clock.paused, "ordinary resume for motion observation") var minute: int = clock.absolute_minute while clock.absolute_minute == minute and not terminated: await tree.process_frame await frames() expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture") expect(stream.capture("active-motion", runtime, main.world_preview), "active interpolation history") await button(main.pause_button) expect(clock.paused, "ordinary pause for stable observation") await frames() expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture") expect(stream.capture("paused-motion", runtime, main.world_preview), "paused interpolation history") class InputObserver extends Node: var events: Array = [] var endpoint: RefCounted = null func _input(event: InputEvent) -> void: if event is InputEventMouseButton or event is InputEventKey or event is InputEventMouseMotion: 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() var view: WorldPreview = main.world_preview expect(await stream.await_draws(tree, view), "real draw history before capture") action["report"] = Codec.new().typed(view.get_life_observation_report()) action["viewport_to_screen"] = affine(view.get_screen_transform() * view.get_global_transform_with_canvas().affine_inverse()) action["local_to_viewport"] = affine(view.get_global_transform_with_canvas()) action["local_to_screen"] = affine(view.get_screen_transform()) action["rectangles"] = Codec.new().typed(draw_rectangles(view.get_life_observation_report(), view)) action["requested_ticks_usec"] = Time.get_ticks_usec() action["requested_unix_ns"] = int(Time.get_unix_time_from_system() * 1000000000.0) action["screenshot_receipt_schema"] = {"screenshots": [{"path": "evidence-root-relative raw image path", "raw_sha256": "original byte SHA256", "tool_call_id": "actual CUA screenshot call ID"}]} expect(stream.capture(stem, runtime, view), "request state/report") 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("P23_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, "ack_observed_ticks_usec": Time.get_ticks_usec(), "request_sha256": FileAccess.get_sha256(folder.path_join("request.json")), "ack_observed_unix_ns": int(Time.get_unix_time_from_system() * 1000000000.0), "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/" + layout + "/" + stem + "/ack.json") all_requests.append("os-pair/" + layout + "/" + stem + "/ack.json") func annotate(row: Dictionary) -> void: var view: WorldPreview = main.world_preview row["rectangles"] = Codec.new().typed(draw_rectangles(view.get_life_observation_report(), view)) row["viewport_to_screen"] = affine(view.get_screen_transform() * view.get_global_transform_with_canvas().affine_inverse()) row["reveal"] = reveal_rows.duplicate(true) reveal_rows.clear() row["input_method"] = "native_os" row["requests"] = receipts.duplicate() receipts.clear() # Rectangles describe existing draw commands; no second renderer is invoked. func draw_rectangles(report: Dictionary, view: WorldPreview) -> Dictionary: var rows: Array = [] for id: Variant in report.get(&"rooms", {}): var room: Dictionary = report[&"rooms"][id] if room.has(&"rect"): rows.append({"category": "room", "id": id, "rect": room[&"rect"], "style_source": "scripts/ui/pixel_tower_art.gd"}) for index: int in range(report.get(&"landing_badges", []).size()): var badge: Dictionary = report[&"landing_badges"][index] rows.append(badge_rectangle("queue", str(index), badge[&"at"], badge[&"text"])) for cabin: Dictionary in report.get(&"cabins", []): rows.append({"category": "EV", "id": cabin[&"shaft_id"], "rect": cabin[&"rect"], "style_source": "scripts/ui/pixel_tower_art.gd"}) for actor: Dictionary in cabin[&"passengers"]: rows.append(actor_rectangle(actor)) for actor: Dictionary in report.get(&"actor_drawings", []): rows.append(actor_rectangle(actor)) if actor[&"staff"] and report[&"resting_staff"].has(StringName(String(actor[&"key"]).trim_prefix("staff/"))): var rest := actor_rectangle(actor) rest["category"] = "rest" rows.append(rest) for index: int in range(report.get(&"actor_aggregations", []).size()): var badge: Dictionary = report[&"actor_aggregations"][index] rows.append(badge_rectangle("aggregation", str(index), badge[&"at"], badge[&"text"])) for index: int in range(report.get(&"badges", []).size()): var badge: Dictionary = report[&"badges"][index] rows.append(badge_rectangle("goods" if badge[&"kind"] == &"supply" else "business", str(index), badge[&"at"], badge[&"text"])) for entry: Dictionary in report.get(&"traffic", []): var traffic: Dictionary = entry[&"traffic"] if traffic[&"current_density"] > 0 or traffic[&"temporary_block"]: var cell: Vector3i = entry[&"cell"] rows.append({"category": "traffic", "id": str(cell.x) + "," + str(cell.y) + "," + str(cell.z), "rect": entry[&"rect"], "style_source": "scenes/main/world_preview.gd"}) var transform := view.get_global_transform_with_canvas() for row: Dictionary in rows: row["viewport_rect"] = transform * (row["rect"] as Rect2) row["screen_rect"] = view.get_screen_transform() * (row["rect"] as Rect2) var source: String = FileAccess.get_file_as_string("res://" + row["style_source"]) var regex := RegEx.new() regex.compile('Color\\("([0-9a-fA-F]{6})"') var colors: Array = [] for found: RegExMatch in regex.search_all(source): if not colors.has(found.get_string(1)): colors.append(found.get_string(1)) row["style_colors"] = colors return {"rows": rows} func badge_rectangle(category: String, id: String, at: Vector2, text: String) -> Dictionary: var width := ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10).x + 6 return {"category": category, "id": id, "rect": Rect2(at, Vector2(text.length() * 10 + 6, 14)), "text": text, "font_width": width - 6, "style_source": "scenes/main/world_preview.gd"} func actor_rectangle(actor: Dictionary) -> Dictionary: return {"category": "staff" if actor[&"staff"] else "guest", "id": actor[&"key"], "rect": Rect2(actor[&"at"], Vector2(16, 18) * float(actor[&"scale"])), "style_source": "scripts/ui/pixel_people.gd"} 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 setup() -> void: await super.setup() input_method = "native_os" expect(DisplayServer.get_name() != "headless", "real OS display") tree.root.title = "The Tree P23-03 " + layout + " " + role + " PID " + str(OS.get_process_id()) input_observer = InputObserver.new() # No handler dispatch: this observer only records incoming OS events. tree.root.add_child(input_observer) 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()}) func button(node: BaseButton) -> void: expect(node != null and not node.disabled, "reachable ordinary button") if node == null or node.disabled: return await request({"action": "click", "point": point(node), "button_text": str(node.get("text"))}) func load_menu(menu: MenuButton) -> void: 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 screenshot(label: String) -> void: await frames() await request({"action": "screenshot", "id": label, "layout": layout}) func layer_observations(prefix: String) -> void: if role == "writer" and prefix == "writer": await screenshot("calibration") await button(main.front_button) await screenshot(prefix + "-FRONT") await button(main.back_button) await screenshot(prefix + "-BACK") if role == "writer" and prefix == "writer": await ordinary_camera() func ordinary_camera() -> void: var view: WorldPreview = main.world_preview var at := point(view) expect(not view.pan_mode, "ordinary zoom starts with move mode off") var zoom: float = view.zoom_level await request({"action": "wheel", "point": at, "direction": "down", "ctrl": false, "move_mode": false}) expect(view.zoom_level != zoom, "ordinary wheel changes zoom") await screenshot("ordinary-wheel-zoom") await button(main.pan_button) await screenshot("pan-mode-button") expect(view.pan_mode, "PanModeButton enables move mode") zoom = view.zoom_level var pan: Vector2 = view.pan_offset await request({"action": "wheel", "point": point(view), "direction": "up", "ctrl": false, "move_mode": true}) expect(view.pan_offset != pan and view.zoom_level == zoom, "ordinary move-mode wheel pans") await screenshot("move-mode-wheel-pan") func finish_observations() -> void: json_file(role + "-requests.json", {"requests": all_requests, "pid": OS.get_process_id()}) if is_instance_valid(input_observer): input_observer.queue_free() super.finish_observations()