244 lines
10 KiB
GDScript
244 lines
10 KiB
GDScript
@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")
|
|
const RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
|
|
const WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT := preload("res://src/render/environment/world_environment_snapshot.gd")
|
|
const ENTITY_PRESENTATION_SNAPSHOT_SCRIPT := preload("res://src/render/entities/entity_presentation_snapshot.gd")
|
|
const ENTITY_ID_SCRIPT := preload("res://src/domain/identity/entity_id.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("..")
|
|
|
|
## Scene-owned outdoor environment implementation resolved relative to this node.
|
|
## The facade delegates snapshots but owns neither the controller nor Environment.
|
|
@export var world_environment_controller_path: NodePath = NodePath("../WowSkyController")
|
|
|
|
## Scene-owned service that creates and owns session-local world entity visuals.
|
|
@export var world_entity_presenter_path: NodePath = NodePath("../WorldEntityPresenter")
|
|
|
|
## 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 _world_environment_controller: Node
|
|
var _world_entity_presenter: Node
|
|
var _missing_loader_reported := false
|
|
var _missing_environment_controller_reported := false
|
|
var _missing_entity_presenter_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)
|
|
|
|
|
|
## Samples the currently loaded render terrain at a typed Godot world position.
|
|
## This read-only renderer view is diagnostic and does not replace authoritative
|
|
## gameplay collision or the independently composed [TerrainQuery].
|
|
func sample_ground_height(godot_world_position: GodotWorldPosition) -> RefCounted:
|
|
var loader := _resolve_streaming_world_loader()
|
|
if loader == null:
|
|
return RENDERED_GROUND_SAMPLE_SCRIPT.unavailable(&"render_facade_unavailable")
|
|
var sample_variant = loader.call("sample_rendered_ground_height", godot_world_position)
|
|
if sample_variant is RefCounted and sample_variant.get_script() == RENDERED_GROUND_SAMPLE_SCRIPT:
|
|
return sample_variant
|
|
push_error("WorldRenderFacade received an invalid rendered ground sample.")
|
|
return RENDERED_GROUND_SAMPLE_SCRIPT.unavailable(&"render_ground_sample_invalid")
|
|
|
|
|
|
## Returns a detached diagnostic snapshot for one rendered-ground query.
|
|
## Mutating the result cannot mutate streamer queues, tile state or resources.
|
|
func renderer_ground_query_snapshot(godot_world_position: GodotWorldPosition) -> Dictionary:
|
|
var loader := _resolve_streaming_world_loader()
|
|
if loader == null:
|
|
return {}
|
|
var snapshot_variant = loader.call("render_ground_query_snapshot", godot_world_position)
|
|
if not (snapshot_variant is Dictionary):
|
|
push_error("WorldRenderFacade received a non-Dictionary ground query snapshot.")
|
|
return {}
|
|
return (snapshot_variant as Dictionary).duplicate(true)
|
|
|
|
|
|
## Applies an immutable environment snapshot through the renderer-owned sky path.
|
|
## Returns false for an invalid value or unavailable/incompatible controller.
|
|
func apply_environment_snapshot(world_environment_snapshot: RefCounted) -> bool:
|
|
if (
|
|
world_environment_snapshot == null
|
|
or world_environment_snapshot.get_script() != WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT
|
|
or not bool(world_environment_snapshot.get("is_valid"))
|
|
):
|
|
return false
|
|
var controller := _resolve_world_environment_controller()
|
|
if controller == null:
|
|
return false
|
|
return bool(controller.call("apply_environment_snapshot", world_environment_snapshot))
|
|
|
|
|
|
## Idempotently creates or updates one world entity visual through the renderer-
|
|
## owned presenter. The immutable full snapshot remains caller-owned.
|
|
func present_entity(entity_presentation_snapshot: RefCounted) -> bool:
|
|
if (
|
|
entity_presentation_snapshot == null
|
|
or entity_presentation_snapshot.get_script() != ENTITY_PRESENTATION_SNAPSHOT_SCRIPT
|
|
or not bool(entity_presentation_snapshot.get("is_valid"))
|
|
):
|
|
return false
|
|
var presenter := _resolve_world_entity_presenter()
|
|
if presenter == null:
|
|
return false
|
|
return bool(presenter.call("present_entity", entity_presentation_snapshot))
|
|
|
|
|
|
## Removes one session-local entity visual without changing authoritative state.
|
|
func remove_entity(entity_id: RefCounted) -> bool:
|
|
if entity_id == null or entity_id.get_script() != ENTITY_ID_SCRIPT or not bool(entity_id.call("is_valid")):
|
|
return false
|
|
var presenter := _resolve_world_entity_presenter()
|
|
if presenter == null:
|
|
return false
|
|
return bool(presenter.call("remove_entity", entity_id))
|
|
|
|
|
|
## Returns detached entity-presentation diagnostics with no Node references.
|
|
func entity_presentation_snapshot() -> Dictionary:
|
|
var presenter := _resolve_world_entity_presenter()
|
|
if presenter == null:
|
|
return {}
|
|
var snapshot_variant = presenter.call("entity_presentation_snapshot")
|
|
if not (snapshot_variant is Dictionary):
|
|
push_error("WorldRenderFacade received a non-Dictionary entity presentation snapshot.")
|
|
return {}
|
|
return (snapshot_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",
|
|
&"sample_rendered_ground_height",
|
|
&"render_ground_query_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
|
|
|
|
|
|
func _resolve_world_environment_controller() -> Node:
|
|
if is_instance_valid(_world_environment_controller):
|
|
return _world_environment_controller
|
|
var candidate := get_node_or_null(world_environment_controller_path)
|
|
if candidate == null or not candidate.has_method(&"apply_environment_snapshot"):
|
|
if not _missing_environment_controller_reported:
|
|
push_error(
|
|
"WorldRenderFacade cannot resolve a compatible environment controller at %s"
|
|
% world_environment_controller_path
|
|
)
|
|
_missing_environment_controller_reported = true
|
|
return null
|
|
_world_environment_controller = candidate
|
|
_missing_environment_controller_reported = false
|
|
return _world_environment_controller
|
|
|
|
|
|
func _resolve_world_entity_presenter() -> Node:
|
|
if is_instance_valid(_world_entity_presenter):
|
|
return _world_entity_presenter
|
|
var candidate := get_node_or_null(world_entity_presenter_path)
|
|
if candidate == null:
|
|
if not _missing_entity_presenter_reported:
|
|
push_error("WorldRenderFacade cannot resolve entity presenter at %s" % world_entity_presenter_path)
|
|
_missing_entity_presenter_reported = true
|
|
return null
|
|
for required_method in [&"present_entity", &"remove_entity", &"entity_presentation_snapshot"]:
|
|
if not candidate.has_method(required_method):
|
|
if not _missing_entity_presenter_reported:
|
|
push_error("WorldRenderFacade entity presenter lacks %s" % required_method)
|
|
_missing_entity_presenter_reported = true
|
|
return null
|
|
_world_entity_presenter = candidate
|
|
_missing_entity_presenter_reported = false
|
|
return _world_entity_presenter
|