rnd(M03): introduce renderer facade seam
Work-Package: M03-RND-FACADE-001 Agent: sindo-main-codex Tests: facade/focus/coordinate/manifest contracts, renderer dry-run, coordination and documentation gates Fidelity: focus targets, budgets, queues, caches and visible renderer behavior remain unchanged
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
@tool
|
||||
class_name WorldRenderFacade
|
||||
extends Node
|
||||
|
||||
## Stable main-thread boundary between runtime/tool callers and the monolithic
|
||||
## streaming renderer. The facade owns no queues, caches, nodes or RIDs.
|
||||
|
||||
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
|
||||
|
||||
## Internal renderer implementation resolved relative to this node.
|
||||
## The referenced node remains owned by its scene parent.
|
||||
@export var streaming_world_loader_path: NodePath = NodePath("..")
|
||||
|
||||
## Optional explicit Node3D source sampled in Godot world coordinates.
|
||||
## Runtime scenes use the player; capture and probe tools replace it with their camera.
|
||||
@export var streaming_focus_source_path: NodePath
|
||||
|
||||
var _streaming_world_loader: Node
|
||||
var _missing_loader_reported := false
|
||||
var _missing_focus_source_reported := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_resolve_streaming_world_loader()
|
||||
set_process(streaming_focus_source_path != NodePath())
|
||||
if streaming_focus_source_path != NodePath():
|
||||
# Child nodes become ready before their parent StreamingWorldLoader. Only
|
||||
# publish the initial focus here; the loader applies it after initialization.
|
||||
_capture_streaming_focus_from_source()
|
||||
|
||||
|
||||
func _process(_delta_seconds: float) -> void:
|
||||
_capture_streaming_focus_from_source()
|
||||
|
||||
|
||||
## Replaces the renderer focus. The immutable value remains caller-owned; the
|
||||
## internal streamer retains the reference for its next throttled refresh.
|
||||
func set_streaming_focus(streaming_focus: StreamingFocus) -> void:
|
||||
var loader := _resolve_streaming_world_loader()
|
||||
if loader == null:
|
||||
return
|
||||
loader.call("set_streaming_focus", streaming_focus)
|
||||
|
||||
|
||||
## Samples the configured source and requests target refresh from the internal
|
||||
## streamer. Returns false when either dependency or a valid focus is unavailable.
|
||||
func refresh_streaming_focus(force: bool = false) -> bool:
|
||||
_capture_streaming_focus_from_source()
|
||||
var loader := _resolve_streaming_world_loader()
|
||||
if loader == null:
|
||||
return false
|
||||
return bool(loader.call("refresh_streaming_focus", force))
|
||||
|
||||
|
||||
## Returns a detached read-only metrics snapshot for diagnostics and baselines.
|
||||
## Mutating the returned Dictionary cannot mutate renderer-owned state.
|
||||
func renderer_metrics_snapshot() -> Dictionary:
|
||||
var loader := _resolve_streaming_world_loader()
|
||||
if loader == null:
|
||||
return {}
|
||||
var metrics_variant = loader.call("render_baseline_snapshot")
|
||||
if not (metrics_variant is Dictionary):
|
||||
push_error("WorldRenderFacade received a non-Dictionary renderer metrics snapshot.")
|
||||
return {}
|
||||
return (metrics_variant as Dictionary).duplicate(true)
|
||||
|
||||
|
||||
func _capture_streaming_focus_from_source() -> void:
|
||||
if streaming_focus_source_path == NodePath():
|
||||
return
|
||||
var focus_source := get_node_or_null(streaming_focus_source_path) as Node3D
|
||||
if focus_source == null:
|
||||
if not _missing_focus_source_reported:
|
||||
push_warning("WorldRenderFacade focus source is missing or is not Node3D: %s" % streaming_focus_source_path)
|
||||
_missing_focus_source_reported = true
|
||||
return
|
||||
|
||||
_missing_focus_source_reported = false
|
||||
var godot_position: Vector3 = focus_source.global_position
|
||||
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(
|
||||
godot_position.x,
|
||||
godot_position.y,
|
||||
godot_position.z
|
||||
)
|
||||
set_streaming_focus(STREAMING_FOCUS_SCRIPT.new(typed_world_position))
|
||||
|
||||
|
||||
func _resolve_streaming_world_loader() -> Node:
|
||||
if is_instance_valid(_streaming_world_loader):
|
||||
return _streaming_world_loader
|
||||
var candidate := get_node_or_null(streaming_world_loader_path)
|
||||
if candidate == null:
|
||||
if not _missing_loader_reported:
|
||||
push_error("WorldRenderFacade cannot resolve StreamingWorldLoader at %s" % streaming_world_loader_path)
|
||||
_missing_loader_reported = true
|
||||
return null
|
||||
for required_method in [
|
||||
&"set_streaming_focus",
|
||||
&"refresh_streaming_focus",
|
||||
&"render_baseline_snapshot",
|
||||
]:
|
||||
if not candidate.has_method(required_method):
|
||||
if not _missing_loader_reported:
|
||||
push_error("WorldRenderFacade renderer implementation lacks %s" % required_method)
|
||||
_missing_loader_reported = true
|
||||
return null
|
||||
_streaming_world_loader = candidate
|
||||
_missing_loader_reported = false
|
||||
return _streaming_world_loader
|
||||
@@ -0,0 +1 @@
|
||||
uid://sxn3ai2ic18r
|
||||
@@ -6,6 +6,7 @@
|
||||
[ext_resource type="Script" uid="uid://cq60al4uw0mgo" path="res://src/scenes/player/third_person_camera_rig.gd" id="4_camera_rig"]
|
||||
[ext_resource type="Script" uid="uid://bhqbo1ftrt3a3" path="res://src/scenes/character/character_appearance_presenter.gd" id="5_appearance"]
|
||||
[ext_resource type="Script" uid="uid://c2jnn0e3mmymc" path="res://src/scenes/character/character_animation_presenter.gd" id="6_animation"]
|
||||
[ext_resource type="Script" path="res://src/render/world_render_facade.gd" id="7_render_facade"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="PlayerCapsuleShape_1"]
|
||||
radius = 0.45
|
||||
@@ -56,7 +57,6 @@ fog_depth_end = 5200.0
|
||||
|
||||
[node name="StreamingWorld" type="Node3D" unique_id=1063159974]
|
||||
script = ExtResource("1_stream")
|
||||
streaming_focus_source_path = NodePath("ThirdPersonPlayer")
|
||||
camera_path = NodePath("ThirdPersonPlayer/CameraPivot/Camera3D")
|
||||
quality_preset = "High"
|
||||
update_interval = 0.1
|
||||
@@ -94,6 +94,11 @@ wmo_visibility_range = 3600.0
|
||||
debug_streaming = true
|
||||
hitch_profiler_enabled = true
|
||||
|
||||
[node name="WorldRenderFacade" type="Node" parent="."]
|
||||
script = ExtResource("7_render_facade")
|
||||
streaming_world_loader_path = NodePath("..")
|
||||
streaming_focus_source_path = NodePath("../ThirdPersonPlayer")
|
||||
|
||||
[node name="ThirdPersonPlayer" type="CharacterBody3D" parent="." unique_id=502573687]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 16800, 80, 26400)
|
||||
script = ExtResource("2_player")
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[ext_resource type="Script" path="res://src/scenes/player/third_person_camera_rig.gd" id="4_camera_rig"]
|
||||
[ext_resource type="Script" path="res://src/scenes/character/character_appearance_presenter.gd" id="5_appearance"]
|
||||
[ext_resource type="Script" path="res://src/scenes/character/character_animation_presenter.gd" id="6_animation"]
|
||||
[ext_resource type="Script" path="res://src/render/world_render_facade.gd" id="7_render_facade"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="PlayerCapsuleShape_1"]
|
||||
radius = 0.45
|
||||
@@ -57,7 +58,6 @@ fog_depth_end = 5200.0
|
||||
[node name="StreamingWorld" type="Node3D" unique_id=1063159974]
|
||||
script = ExtResource("1_sisqv")
|
||||
map_name = "Kalimdor"
|
||||
streaming_focus_source_path = NodePath("ThirdPersonPlayer")
|
||||
camera_path = NodePath("ThirdPersonPlayer/CameraPivot/Camera3D")
|
||||
quality_preset = "High"
|
||||
update_interval = 0.1
|
||||
@@ -95,6 +95,11 @@ wmo_visibility_range = 3600.0
|
||||
debug_streaming = true
|
||||
hitch_profiler_enabled = true
|
||||
|
||||
[node name="WorldRenderFacade" type="Node" parent="."]
|
||||
script = ExtResource("7_render_facade")
|
||||
streaming_world_loader_path = NodePath("..")
|
||||
streaming_focus_source_path = NodePath("../ThirdPersonPlayer")
|
||||
|
||||
[node name="ThirdPersonPlayer" type="CharacterBody3D" parent="." unique_id=502573687]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 16800, 80, 26400)
|
||||
script = ExtResource("2_puy8r")
|
||||
|
||||
@@ -64,7 +64,12 @@ func _capture_async() -> void:
|
||||
camera.far = 50000.0
|
||||
camera.position = _vector3(first.get("camera", [0.0, 0.0, 0.0]))
|
||||
(world as Node3D).add_child(camera)
|
||||
world.set("streaming_focus_source_path", NodePath("CheckpointCamera"))
|
||||
var render_facade := world.get_node_or_null("WorldRenderFacade")
|
||||
if render_facade == null:
|
||||
push_error("Streaming scene has no WorldRenderFacade")
|
||||
quit(1)
|
||||
return
|
||||
render_facade.set("streaming_focus_source_path", NodePath("../CheckpointCamera"))
|
||||
world.set("debug_streaming", true)
|
||||
world.set("runtime_stats_enabled", true)
|
||||
get_root().add_child(world)
|
||||
@@ -126,8 +131,7 @@ func _capture_async() -> void:
|
||||
if player != null:
|
||||
player.global_position = _vector3(checkpoint.get("player", checkpoint.get("target", [0.0, 0.0, 0.0])))
|
||||
_set_sky_time(world, float(checkpoint.get("time_hours", 13.0)))
|
||||
if world.has_method("refresh_streaming_focus"):
|
||||
world.call("refresh_streaming_focus", true)
|
||||
render_facade.call("refresh_streaming_focus", true)
|
||||
|
||||
if dry_run:
|
||||
print("RENDER_CHECKPOINT dry_run name=%s coverage=%s camera=%s target=%s yaw_offset=%.2f pitch_offset=%.2f time=%.2f" % [
|
||||
@@ -148,7 +152,7 @@ func _capture_async() -> void:
|
||||
var load_started := Time.get_ticks_usec()
|
||||
await create_timer(maxf(wait_seconds, 0.0)).timeout
|
||||
var load_time_ms := float(Time.get_ticks_usec() - load_started) / 1000.0
|
||||
var metrics := await _measure_frames(maxf(measure_seconds, 0.1), world)
|
||||
var metrics := await _measure_frames(maxf(measure_seconds, 0.1), render_facade)
|
||||
await RenderingServer.frame_post_draw
|
||||
|
||||
var image := get_root().get_texture().get_image()
|
||||
@@ -189,7 +193,7 @@ func _capture_async() -> void:
|
||||
quit(0)
|
||||
|
||||
|
||||
func _measure_frames(seconds: float, world: Node) -> Dictionary:
|
||||
func _measure_frames(seconds: float, render_facade: Node) -> Dictionary:
|
||||
var frame_times: Array[float] = []
|
||||
var deadline := Time.get_ticks_usec() + int(seconds * 1000000.0)
|
||||
var previous := Time.get_ticks_usec()
|
||||
@@ -200,8 +204,8 @@ func _measure_frames(seconds: float, world: Node) -> Dictionary:
|
||||
previous = now
|
||||
frame_times.sort()
|
||||
var snapshot := {}
|
||||
if world.has_method("render_baseline_snapshot"):
|
||||
snapshot = world.call("render_baseline_snapshot")
|
||||
if render_facade.has_method("renderer_metrics_snapshot"):
|
||||
snapshot = render_facade.call("renderer_metrics_snapshot")
|
||||
return {
|
||||
"frames": frame_times.size(),
|
||||
"frame_ms_p50": _percentile(frame_times, 0.50),
|
||||
|
||||
@@ -28,7 +28,12 @@ func _run_async() -> void:
|
||||
camera.name = "OccluderProbeCamera"
|
||||
camera.current = true
|
||||
world.add_child(camera)
|
||||
world.set("streaming_focus_source_path", NodePath(camera.name))
|
||||
var render_facade := world.get_node_or_null("WorldRenderFacade")
|
||||
if render_facade == null:
|
||||
push_error("CAMERA_OCCLUDER_PROBE: streaming scene has no WorldRenderFacade")
|
||||
quit(1)
|
||||
return
|
||||
render_facade.set("streaming_focus_source_path", NodePath("../%s" % camera.name))
|
||||
world.set("debug_streaming", false)
|
||||
get_root().add_child(world)
|
||||
await process_frame
|
||||
@@ -47,8 +52,7 @@ func _run_async() -> void:
|
||||
var camera_position := _vector3(checkpoint.get("camera", []))
|
||||
var target_position := _vector3(checkpoint.get("target", []))
|
||||
camera.global_position = camera_position
|
||||
if world.has_method("refresh_streaming_focus"):
|
||||
world.call("refresh_streaming_focus", true)
|
||||
render_facade.call("refresh_streaming_focus", true)
|
||||
await create_timer(maxf(0.1, wait_seconds)).timeout
|
||||
var geometry_nodes: Array[Node3D] = []
|
||||
_collect_geometry_nodes(world, geometry_nodes)
|
||||
|
||||
@@ -37,7 +37,12 @@ func _run_async() -> void:
|
||||
var camera := Camera3D.new()
|
||||
camera.current = true
|
||||
world.add_child(camera)
|
||||
world.set("streaming_focus_source_path", NodePath(camera.name))
|
||||
var render_facade := world.get_node_or_null("WorldRenderFacade")
|
||||
if render_facade == null:
|
||||
push_error("TERRAIN_HEIGHT_PROBE: streaming scene has no WorldRenderFacade")
|
||||
quit(1)
|
||||
return
|
||||
render_facade.set("streaming_focus_source_path", NodePath("../%s" % camera.name))
|
||||
world.set("debug_streaming", false)
|
||||
get_root().add_child(world)
|
||||
await process_frame
|
||||
@@ -54,8 +59,7 @@ func _run_async() -> void:
|
||||
continue
|
||||
var camera_position := _vector3(checkpoint.get("camera", []))
|
||||
camera.global_position = camera_position
|
||||
if world.has_method("refresh_streaming_focus"):
|
||||
world.call("refresh_streaming_focus", true)
|
||||
render_facade.call("refresh_streaming_focus", true)
|
||||
await create_timer(maxf(0.1, wait_seconds)).timeout
|
||||
var terrain_sample := _sample_terrain(world, camera_position)
|
||||
terrain_sample["name"] = checkpoint.get("name", "checkpoint")
|
||||
|
||||
@@ -6,6 +6,7 @@ const StreamingFocusScript = preload("res://src/domain/streaming/streaming_focus
|
||||
const GodotWorldPositionScript = preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
const FACADE_PATH := "res://src/render/world_render_facade.gd"
|
||||
const RUNTIME_SCENE_PATHS: Array[String] = [
|
||||
"res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
|
||||
"res://src/scenes/streaming/kalimdor_streaming.tscn",
|
||||
@@ -21,6 +22,7 @@ func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_scene_free_focus_value(failures)
|
||||
_verify_loader_boundary(failures)
|
||||
_verify_facade_boundary(failures)
|
||||
_verify_runtime_scene_wiring(failures)
|
||||
_verify_capture_tool_wiring(failures)
|
||||
|
||||
@@ -65,11 +67,21 @@ func _verify_loader_boundary(failures: Array[String]) -> void:
|
||||
_expect_true(not loader_source.contains(forbidden_text), "loader omits %s" % forbidden_text, failures)
|
||||
|
||||
|
||||
func _verify_facade_boundary(failures: Array[String]) -> void:
|
||||
var facade_source := _read_text(FACADE_PATH, failures)
|
||||
for required_text in [
|
||||
"class_name WorldRenderFacade",
|
||||
"func set_streaming_focus(streaming_focus: StreamingFocus) -> void:",
|
||||
"func refresh_streaming_focus(force: bool = false) -> bool:",
|
||||
]:
|
||||
_expect_true(facade_source.contains(required_text), "facade contains %s" % required_text, failures)
|
||||
|
||||
|
||||
func _verify_runtime_scene_wiring(failures: Array[String]) -> void:
|
||||
for scene_path in RUNTIME_SCENE_PATHS:
|
||||
var scene_source := _read_text(scene_path, failures)
|
||||
_expect_true(
|
||||
scene_source.contains('streaming_focus_source_path = NodePath("ThirdPersonPlayer")'),
|
||||
scene_source.contains('streaming_focus_source_path = NodePath("../ThirdPersonPlayer")'),
|
||||
"%s uses player focus" % scene_path,
|
||||
failures
|
||||
)
|
||||
@@ -78,8 +90,8 @@ func _verify_runtime_scene_wiring(failures: Array[String]) -> void:
|
||||
func _verify_capture_tool_wiring(failures: Array[String]) -> void:
|
||||
for tool_path in CAPTURE_TOOL_PATHS:
|
||||
var tool_source := _read_text(tool_path, failures)
|
||||
_expect_true(tool_source.contains('world.set("streaming_focus_source_path"'), "%s sets explicit focus source" % tool_path, failures)
|
||||
_expect_true(tool_source.contains('world.call("refresh_streaming_focus", true)'), "%s uses public refresh" % tool_path, failures)
|
||||
_expect_true(tool_source.contains('render_facade.set("streaming_focus_source_path"'), "%s sets explicit focus source" % tool_path, failures)
|
||||
_expect_true(tool_source.contains('render_facade.call("refresh_streaming_focus", true)'), "%s uses facade refresh" % tool_path, failures)
|
||||
_expect_true(not tool_source.contains('world.call("_refresh_streaming_targets_at"'), "%s avoids private refresh" % tool_path, failures)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
extends SceneTree
|
||||
|
||||
## Headless contract and wiring regression for the first M03 renderer facade seam.
|
||||
|
||||
const WORLD_RENDER_FACADE_SCRIPT := preload("res://src/render/world_render_facade.gd")
|
||||
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
|
||||
|
||||
const RUNTIME_SCENE_PATHS: Array[String] = [
|
||||
"res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
|
||||
"res://src/scenes/streaming/kalimdor_streaming.tscn",
|
||||
]
|
||||
const TOOL_PATHS: Array[String] = [
|
||||
"res://src/tools/capture_render_checkpoints.gd",
|
||||
"res://src/tools/probe_render_camera_occluders.gd",
|
||||
"res://src/tools/probe_render_terrain_height.gd",
|
||||
]
|
||||
|
||||
|
||||
class StreamingWorldLoaderDouble extends Node:
|
||||
var current_focus: StreamingFocus
|
||||
var refresh_count := 0
|
||||
var metrics := {"tiles": 2, "queues": {"tile": 1}}
|
||||
|
||||
func set_streaming_focus(streaming_focus: StreamingFocus) -> void:
|
||||
current_focus = streaming_focus
|
||||
|
||||
func refresh_streaming_focus(_force: bool = false) -> bool:
|
||||
refresh_count += 1
|
||||
return current_focus != null
|
||||
|
||||
func render_baseline_snapshot() -> Dictionary:
|
||||
return metrics
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
await _verify_delegation_and_snapshot_isolation(failures)
|
||||
_verify_repository_wiring(failures)
|
||||
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("WORLD_RENDER_FACADE: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("WORLD_RENDER_FACADE PASS delegation=3 runtime_scenes=2 tools=3")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_delegation_and_snapshot_isolation(failures: Array[String]) -> void:
|
||||
var test_root := Node.new()
|
||||
var loader := StreamingWorldLoaderDouble.new()
|
||||
loader.name = "StreamingWorldLoaderDouble"
|
||||
var source := Node3D.new()
|
||||
source.name = "FocusSource"
|
||||
source.position = Vector3(10.0, 20.0, 30.0)
|
||||
var facade = WORLD_RENDER_FACADE_SCRIPT.new()
|
||||
facade.name = "WorldRenderFacade"
|
||||
facade.streaming_world_loader_path = NodePath("../StreamingWorldLoaderDouble")
|
||||
facade.streaming_focus_source_path = NodePath("../FocusSource")
|
||||
test_root.add_child(loader)
|
||||
test_root.add_child(source)
|
||||
test_root.add_child(facade)
|
||||
get_root().add_child(test_root)
|
||||
await process_frame
|
||||
|
||||
_expect_true(loader.current_focus != null, "ready captures explicit source", failures)
|
||||
if loader.current_focus != null:
|
||||
_expect_near(loader.current_focus.world_position.x_units, 10.0, "source X", failures)
|
||||
_expect_near(loader.current_focus.world_position.y_units, 20.0, "source Y", failures)
|
||||
_expect_near(loader.current_focus.world_position.z_units, 30.0, "source Z", failures)
|
||||
|
||||
var typed_focus = STREAMING_FOCUS_SCRIPT.new(GODOT_WORLD_POSITION_SCRIPT.new(40.0, 50.0, 60.0))
|
||||
facade.streaming_focus_source_path = NodePath()
|
||||
facade.set_streaming_focus(typed_focus)
|
||||
_expect_true(facade.refresh_streaming_focus(true), "typed focus refresh delegates", failures)
|
||||
_expect_true(loader.current_focus == typed_focus, "typed focus reference delegates", failures)
|
||||
|
||||
var snapshot: Dictionary = facade.renderer_metrics_snapshot()
|
||||
snapshot["tiles"] = 999
|
||||
(snapshot["queues"] as Dictionary)["tile"] = 999
|
||||
_expect_true(int(loader.metrics.tiles) == 2, "top-level metrics are detached", failures)
|
||||
_expect_true(int(loader.metrics.queues.tile) == 1, "nested metrics are detached", failures)
|
||||
test_root.queue_free()
|
||||
|
||||
|
||||
func _verify_repository_wiring(failures: Array[String]) -> void:
|
||||
for scene_path in RUNTIME_SCENE_PATHS:
|
||||
var scene_source := _read_text(scene_path, failures)
|
||||
_expect_true(scene_source.contains('path="res://src/render/world_render_facade.gd"'), "%s loads facade" % scene_path, failures)
|
||||
_expect_true(scene_source.contains('[node name="WorldRenderFacade" type="Node" parent="."]'), "%s owns facade node" % scene_path, failures)
|
||||
_expect_true(scene_source.contains('streaming_focus_source_path = NodePath("../ThirdPersonPlayer")'), "%s facade uses player focus" % scene_path, failures)
|
||||
|
||||
for tool_path in TOOL_PATHS:
|
||||
var tool_source := _read_text(tool_path, failures)
|
||||
_expect_true(tool_source.contains('get_node_or_null("WorldRenderFacade")'), "%s resolves facade" % tool_path, failures)
|
||||
_expect_true(not tool_source.contains('world.call("refresh_streaming_focus"'), "%s avoids direct streamer refresh" % tool_path, failures)
|
||||
_expect_true(not tool_source.contains('world.set("streaming_focus_source_path"'), "%s avoids direct streamer focus config" % tool_path, failures)
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_near(actual_value: float, expected_value: float, label: String, failures: Array[String]) -> void:
|
||||
if not is_equal_approx(actual_value, expected_value):
|
||||
failures.append("%s expected %.3f, got %.3f" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://baoiewiesd02u
|
||||
Reference in New Issue
Block a user