166 lines
7.7 KiB
GDScript
166 lines
7.7 KiB
GDScript
extends RefCounted
|
|
## Sparse whole typed captures. No acceptance flags and no truncation.
|
|
const Codec := preload("res://tests/fixtures/p08-02/mixed_world.gd")
|
|
var directory: String
|
|
var manifest: FileAccess
|
|
var observer: MotionObserver
|
|
|
|
class MotionObserver extends Node:
|
|
var view: WorldPreview
|
|
var frames: Array = []
|
|
var elapsed := 0.0
|
|
var scaled_elapsed := 0.0
|
|
func _ready() -> void:
|
|
RenderingServer.frame_post_draw.connect(after_draw)
|
|
view.draw.connect(on_view_draw)
|
|
func _process(delta: float) -> void:
|
|
elapsed += delta
|
|
var clock := get_node_or_null("/root/GameClock")
|
|
if clock == null or not bool(clock.get("paused")):
|
|
scaled_elapsed += delta * maxf(float(clock.get("speed")) if clock != null else 1.0, 0.0)
|
|
func on_view_draw() -> void:
|
|
# Read the draw-consumed report after the real _draw completes.
|
|
after_draw.call_deferred()
|
|
func display_sample(report: Dictionary) -> Dictionary:
|
|
var sample := {}
|
|
for key: String in ["animation_time", "paused", "layer", "zoom", "pan", "viewport", "actor_tracks", "elevator_tracks", "actor_drawings", "cabins"]:
|
|
sample[key] = report[key]
|
|
return sample
|
|
func after_draw() -> void:
|
|
if not is_instance_valid(view): return
|
|
var frame := Engine.get_process_frames()
|
|
if not frames.is_empty() and frame <= int(frames[-1]["frame"]): return
|
|
var report := view.get_life_observation_report()
|
|
if report.is_empty(): return
|
|
var sample := display_sample(report)
|
|
if not frames.is_empty() and sample == frames[-1]["display"] and not sample["paused"]: return
|
|
# Keep only two draw frames, never whole Runtime copies per frame.
|
|
frames.append({"frame": frame, "delta": elapsed, "scaled_delta": scaled_elapsed, "display": sample})
|
|
if frames.size() > 2: frames.pop_front()
|
|
elapsed = 0.0
|
|
scaled_elapsed = 0.0
|
|
func _exit_tree() -> void:
|
|
if is_instance_valid(view) and view.draw.is_connected(on_view_draw): view.draw.disconnect(on_view_draw)
|
|
if RenderingServer.frame_post_draw.is_connected(after_draw): RenderingServer.frame_post_draw.disconnect(after_draw)
|
|
|
|
func watch(view: WorldPreview) -> void:
|
|
observer = MotionObserver.new()
|
|
observer.view = view
|
|
view.add_child(observer)
|
|
|
|
func await_draws(tree: SceneTree, view: WorldPreview, n: int = 2, bound: int = 120) -> bool:
|
|
var start := Engine.get_process_frames()
|
|
for unused: int in range(bound):
|
|
view.queue_redraw()
|
|
await tree.process_frame
|
|
if not is_instance_valid(observer): break
|
|
var report := view.get_life_observation_report()
|
|
if observer.frames.size() == n and not report.is_empty() and int(observer.frames[0]["frame"]) > start and observer.frames[-1]["display"] == observer.display_sample(report):
|
|
return true
|
|
# Persist timeout as a failure, never a skip.
|
|
record("draw-history-timeout", {"start_frame": start, "bound": bound, "required_frames": n})
|
|
return false
|
|
|
|
func open(path: String) -> bool:
|
|
directory = path
|
|
if DirAccess.dir_exists_absolute(path):
|
|
return false
|
|
if DirAccess.make_dir_recursive_absolute(path.path_join("objects")) != OK:
|
|
return false
|
|
manifest = FileAccess.open(path.path_join("manifest.jsonl"), FileAccess.WRITE)
|
|
return manifest != null
|
|
|
|
func object(value: Variant) -> String:
|
|
var text := JSON.stringify(Codec.new().typed(value)) + "\n"
|
|
var digest := text.sha256_text()
|
|
var path := directory.path_join("objects").path_join(digest + ".json")
|
|
if FileAccess.file_exists(path):
|
|
return digest if FileAccess.get_sha256(path) == digest else ""
|
|
var file := FileAccess.open(path, FileAccess.WRITE)
|
|
if file == null:
|
|
return ""
|
|
file.store_string(text)
|
|
file.close()
|
|
return digest if FileAccess.get_sha256(path) == digest else ""
|
|
|
|
func capture(label: String, runtime: Variant, view: WorldPreview) -> bool:
|
|
if manifest == null:
|
|
return false
|
|
var state: Dictionary = runtime.create_snapshot()
|
|
var report := view.get_life_observation_report()
|
|
var state_hash := object(state)
|
|
var report_hash := object(report)
|
|
var audit_hash := object(rest_audit(runtime))
|
|
var motion_hash := object({"frames": observer.frames.duplicate(true) if is_instance_valid(observer) else []})
|
|
var after: Dictionary = runtime.create_snapshot()
|
|
var after_hash := object(after)
|
|
if state_hash.is_empty() or report_hash.is_empty() or audit_hash.is_empty():
|
|
return false
|
|
manifest.store_line(JSON.stringify({"label": label, "pid": OS.get_process_id(),
|
|
"ticks_usec": Time.get_ticks_usec(), "state": state_hash, "report": report_hash,
|
|
"rest_audit": audit_hash, "motion": motion_hash, "state_after": after_hash,
|
|
"input_method": "diagnostic", "acceptance": "pending"}))
|
|
manifest.flush()
|
|
return var_to_bytes(state) == var_to_bytes(after)
|
|
|
|
func close() -> void:
|
|
if manifest != null:
|
|
manifest.close()
|
|
manifest = null
|
|
|
|
# Public selection inputs; Runtime output is only a comparison operand.
|
|
func rest_audit(runtime: Variant) -> Dictionary:
|
|
var rows: Array = []
|
|
for member: StaffRuntime in runtime.staff_service.get_staff_members():
|
|
var memberships: Array = []
|
|
for raid_id: StringName in runtime.defense_service.get_raid_ids():
|
|
var raid: Dictionary = runtime.defense_service.get_raid(raid_id)
|
|
if raid.get(&"combatants", {}).has(member.staff_id):
|
|
memberships.append({"raid_id": raid_id, "state": DefenseService.RaidState.keys()[raid[&"state"]],
|
|
"side": raid[&"combatants"][member.staff_id].get(&"side", &"")})
|
|
var facilities: Array = []
|
|
for room: FacilityInstance in runtime.facility_service.get_instances():
|
|
var probes: Array = []
|
|
var candidates: Dictionary = {}
|
|
var occupied := room.occupied_cells.duplicate()
|
|
if occupied.is_empty(): occupied.append(room.origin)
|
|
for cell: Vector3i in occupied:
|
|
for delta: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]:
|
|
var present: bool = runtime.path_service.has_cell(cell + delta)
|
|
probes.append({"cell": cell + delta, "has_cell": present})
|
|
if present: candidates[cell + delta] = true
|
|
var ordered: Array = candidates.keys()
|
|
ordered.sort_custom(func(a: Vector3i, b: Vector3i) -> bool:
|
|
if a.z != b.z: return a.z < b.z
|
|
if a.y != b.y: return a.y < b.y
|
|
return a.x < b.x)
|
|
var routes: Array = []
|
|
for cell: Vector3i in ordered:
|
|
var path: Array = runtime.path_service.find_path(member.current_cell, cell,
|
|
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"])
|
|
var edges: Array = []
|
|
for index: int in range(1, path.size()):
|
|
# Empty reports remain explicit mismatch evidence.
|
|
edges.append(runtime.path_service.get_edge_report(path[index - 1], path[index], [&"staff"]))
|
|
routes.append({"cell": cell, "path": path, "edges": edges,
|
|
"calculate_path_cost": runtime.path_service.calculate_path_cost(path, [&"staff"]) if not path.is_empty() else -1})
|
|
facilities.append({"instance_id": room.instance_id, "definition_id": room.definition_id,
|
|
"operation_state": FacilityInstance.OperationState.keys()[room.operation_state],
|
|
"origin": room.origin, "occupied_cells": room.occupied_cells.duplicate(),
|
|
"probes": probes, "ordered": ordered, "routes": routes})
|
|
rows.append({"staff_id": member.staff_id, "state": StaffRuntime.State.keys()[member.state],
|
|
"health": member.health, "current_task_id": member.current_task_id, "current_cell": member.current_cell,
|
|
"defense_memberships": memberships, "actor_lock": runtime.event_service.get_actor_locks().has(member.staff_id),
|
|
"in_transit": runtime.is_staff_in_transit(member.staff_id), "facilities": facilities,
|
|
"comparison_only": runtime.get_staff_rest_report(member.staff_id)})
|
|
return {"staff": rows, "path_service_sha256": FileAccess.get_sha256("res://scripts/world/path_service.gd")}
|
|
|
|
func record(label: String, value: Dictionary) -> bool:
|
|
if manifest == null: return false
|
|
var digest := object(value)
|
|
if digest.is_empty(): return false
|
|
manifest.store_line(JSON.stringify({"label": label, "pid": OS.get_process_id(),
|
|
"ticks_usec": Time.get_ticks_usec(), "object": digest}))
|
|
manifest.flush()
|
|
return true
|