Files
TheTree_mock/tests/fixtures/p23-03/passive_observation.gd
T

267 lines
14 KiB
GDScript

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(width, 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()