226 lines
12 KiB
GDScript
226 lines
12 KiB
GDScript
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 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 request(action: Dictionary) -> void:
|
|
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):
|
|
expect(false, "Root CUA acknowledgement timeout")
|
|
tree.quit(1)
|
|
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")):
|
|
expect(false, "Root CUA acknowledgement identity/hash")
|
|
tree.quit(1)
|
|
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["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 reveal(node: Control) -> void:
|
|
await request({"action": "reveal_palette", "node": String(node.get_path()),
|
|
"instructions": "Scroll the normal palette until this button is visible. Do not activate it yet."})
|
|
|
|
func button(node: BaseButton) -> void:
|
|
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:
|
|
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)
|
|
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."})
|
|
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:
|
|
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:
|
|
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:
|
|
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})
|