@tool ## Streams Azeroth terrain around an explicit [StreamingFocus] with chunk-based LODs. extends Node3D const ADT_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/adt_builder.gd") const BAKED_TILE_SCRIPT := preload("res://src/resources/baked_adt_tile.gd") const STREAMING_TILE_SCRIPT := preload("res://src/resources/streaming_adt_tile.gd") const SPLAT_TILE_SCRIPT := preload("res://src/resources/splat_adt_tile.gd") const CONTROL_SPLAT_TILE_SCRIPT := preload("res://src/resources/control_splat_adt_tile.gd") const WMO_STREAMING_SCRIPT := preload("res://src/resources/wmo_streaming_resource.gd") const WMO_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/wmo_builder.gd") const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd") const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd") const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd") const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd") const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd") const REQUIRED_BAKED_TILE_FORMAT_VERSION := 5 const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1 const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3 const M2_MATERIAL_REFRESH_VERSION := 2 const WMO_MATERIAL_REFRESH_VERSION := 10 const TILE_SIZE := 533.33333 const CHUNK_SIZE := TILE_SIZE / 16.0 const ADT_CLIFFROCK_WORLD_YAW_OFFSET := PI const ADT_WATERFALL_WORLD_YAW_OFFSET := PI * 0.5 const QUALITY_CUSTOM := "Custom" const QUALITY_PERFORMANCE := "Performance" const QUALITY_BALANCED := "Balanced" const QUALITY_HIGH := "High" @export var extracted_dir: String = "res://data/extracted" @export var map_name: String = "Azeroth" ## Optional scene adapter source. Any Node3D may provide the runtime focus. @export var streaming_focus_source_path: NodePath ## Camera used only by the optional automatic overview positioning feature. @export var camera_path: NodePath @export_enum("Custom", "Performance", "Balanced", "High") var quality_preset: String = QUALITY_CUSTOM ## Seconds between streaming updates (lower = more responsive, higher = cheaper) @export var update_interval: float = 0.25 ## ADT tiles parsed per update tick (increase for faster initial load, decrease to reduce hitching) @export var tiles_per_tick: int = 2 ## Maximum number of ADT parse tasks running on worker threads at once @export var max_concurrent_tile_tasks: int = 2 ## Chunk mesh nodes created OR removed per update tick @export var chunk_ops_per_tick: int = 24 ## Heavy tile LOD creations per update tick @export var tile_lod_ops_per_tick: int = 1 ## Heavy tile LOD removals per update tick. @export var tile_lod_remove_ops_per_tick: int = 2 ## Loaded tile resources finalized on the main thread per frame. @export var tile_finalize_ops_per_tick: int = 1 ## Full-quality baked terrain tile upgrades finalized per frame. @export var terrain_upgrade_finalize_ops_per_tick: int = 1 ## Full-quality terrain tile resources loaded in parallel. @export var max_concurrent_terrain_upgrade_tasks: int = 1 ## M2/WMO detail batches attached to loaded tiles per frame. @export var detail_asset_ops_per_tick: int = 1 ## M2 MultiMesh groups materialized per frame after background grouping. @export var m2_build_groups_per_tick: int = 2 ## Maximum instances written into a single M2 MultiMesh per frame. @export var m2_multimesh_batch_size: int = 128 ## Experimental GLB M2 animation is allowed only for simple one-primitive creatures. ## Complex doodads (for example GryphonRoost) need a native M2 animator because GLB ## conversion currently breaks their skin/material fidelity. @export var enable_m2_animated_instances: bool = true ## Reject complex animated M2 GLBs until the skinning converter is robust for composite doodads. @export var m2_animated_max_primitives: int = 3 @export var m2_animated_denylist_patterns: PackedStringArray = PackedStringArray(["gryphonroost"]) @export var m2_animated_allowlist_patterns: PackedStringArray = PackedStringArray([ "creature/fish/", "creature/eagle/", "world/critter/", ]) ## Maximum animated M2 scene instances attached per build step. Keep low to avoid hitches. @export var m2_animated_instances_per_tick: int = 8 ## Loaded animated M2 scene resources finalized per frame. @export var m2_animation_finalize_ops_per_tick: int = 1 ## Loaded M2 scene resources converted to meshes per frame. @export var m2_mesh_finalize_ops_per_tick: int = 1 ## WMO instances attached per frame. @export var wmo_build_instances_per_tick: int = 1 ## WMO render-cache mesh groups attached per frame. @export var wmo_render_group_ops_per_tick: int = 24 ## Raw ADT water parse tasks running in parallel for cached terrain tiles. @export var max_concurrent_water_tasks: int = 1 ## Cached-tile water roots attached per frame. @export var water_finalize_ops_per_tick: int = 1 ## Maximum cached WMO .tscn size instantiated while streaming. Larger WMOs need a lightweight render cache. @export var wmo_max_runtime_scene_mb: float = 8.0 ## Minimum camera movement before recomputing streaming targets. @export var streaming_update_distance: float = CHUNK_SIZE * 2.0 ## Recently built tile meshes kept for quick revisits. @export var cached_tile_mesh_limit: int = 24 ## Full-quality terrain meshes kept after an ADT is unloaded, so revisits do not flash back to low quality. @export var terrain_quality_mesh_cache_limit: int = 24 @export var lod0_radius_chunks: int = 6 @export var lod1_radius_chunks: int = 12 @export var lod2_radius_chunks: int = 24 @export var lod2_tile_radius: int = 3 @export var warm_tile_margin: int = 1 ## Extra ADT ring loaded and GPU-warmed outside the visible terrain radius. @export var prewarm_tile_margin: int = 1 @export var prewarm_tile_lod_instances: bool = true ## Extra ADT rings retained after leaving the prewarm radius to avoid boundary churn. @export var retain_tile_margin: int = 2 ## Pre-create hidden full terrain LOD this many ADT widths before it becomes visible. @export var full_lod_prewarm_tiles: float = 1.0 ## Start loading the next ADT-centered ring before crossing a tile boundary. @export_range(0.0, 0.49, 0.01) var boundary_prefetch_threshold: float = 0.35 @export var auto_position_camera: bool = true @export var editor_preview_enabled: bool = true @export var editor_preview_center_x: int = 32 @export var editor_preview_center_y: int = 48 @export var editor_preview_tile_radius: int = 1 @export var editor_follow_view_camera: bool = true @export var editor_persist_generated_nodes: bool = false @export var enable_occlusion_culling: bool = true @export var use_rendering_server_for_terrain: bool = true @export var use_baked_tile_cache: bool = true @export var baked_tile_cache_dir: String = "res://data/cache/baked_terrain_v2" @export var streaming_tile_cache_dir: String = "res://data/cache/baked_terrain_stream_v1" @export var terrain_control_splat_cache_dir: String = "res://data/cache/terrain_control_splat_v3" @export var terrain_splat_cache_dir: String = "res://data/cache/terrain_splat_v1" @export var terrain_full_quality_enabled: bool = true @export var terrain_full_quality_radius_tiles: int = 1 @export var terrain_control_splat_quality_enabled: bool = true @export var terrain_control_splat_primary: bool = true @export var terrain_control_splat_hide_fallback: bool = false @export var terrain_control_splat_hide_fallback_radius_tiles: int = 0 @export var terrain_control_splat_radius_tiles: int = 0 @export var max_active_terrain_control_splat_tiles: int = 5 @export var terrain_control_splat_cache_finalize_ops_per_tick: int = 1 @export var terrain_splat_quality_enabled: bool = true @export var terrain_splat_radius_tiles: int = 0 @export var terrain_splat_lod: int = 0 @export var terrain_splat_runtime_build_enabled: bool = false @export var max_active_terrain_splat_tiles: int = 5 @export var terrain_splat_cache_finalize_ops_per_tick: int = 1 @export var max_concurrent_terrain_splat_tasks: int = 1 @export var terrain_splat_builds_per_tick: int = 1 @export var use_chunk_terrain_lods: bool = false @export var enable_water: bool = false @export var enable_wmo: bool = true @export var enable_m2_assets: bool = true @export var enable_m2_placeholders: bool = false @export var m2_placeholder_only_trees: bool = true ## Dynamic doodad render radius in ADT tiles. Negative keeps all loaded M2s active. @export var m2_tile_radius: int = 1 ## Dynamic WMO render radius in ADT tiles. Negative keeps all loaded WMOs active. @export var wmo_tile_radius: int = 3 ## Godot visibility range applied to instantiated M2 MultiMesh nodes. <= 0 disables it. @export var m2_visibility_range: float = 700.0 ## Godot visibility range applied to instantiated WMO trees. <= 0 disables it. @export var wmo_visibility_range: float = 1800.0 @export var wmo_render_cache_dir: String = "res://data/cache/wmo_render_v1" @export var m2_cache_dir: String = "res://data/cache/m2_glb" @export var wmo_cache_dir: String = "res://data/cache/wmo_tscn" @export var terrain_cast_shadows: bool = false @export var m2_cast_shadows: bool = false @export var wmo_cast_shadows: bool = false @export var editor_reload_now: bool = false @export var debug_streaming: bool = false @export var runtime_stats_enabled: bool = false @export var runtime_stats_interval: float = 2.0 @export var terrain_quality_log_enabled: bool = true @export var terrain_quality_log_interval: float = 1.0 @export var hitch_profiler_enabled: bool = false @export var hitch_profiler_threshold_ms: float = 20.0 var _builder var _map_dir := "" var _abs_map_dir := "" var _wdt_info: Dictionary = {} var _available_tiles: Dictionary = {} var _tile_states: Dictionary = {} var _tile_load_queue: Array = [] var _chunk_remove_queue: Array = [] var _chunk_create_queue: Array = [] var _tile_lod_remove_queue: Array = [] var _tile_lod_create_queue: Array = [] var _detail_asset_queue: Array = [] var _detail_asset_queued: Dictionary = {} var _m2_group_tasks: Dictionary = {} var _m2_group_result_mutex := Mutex.new() var _m2_group_result_queue: Array = [] var _m2_build_jobs: Dictionary = {} var _m2_build_queue: Array = [] var _m2_unique_registry: Dictionary = {} var _m2_mesh_cache: Dictionary = {} var _m2_mesh_load_requests: Dictionary = {} var _m2_mesh_finalize_queue: Array = [] var _m2_runtime_rebuild_required_cache: Dictionary = {} var _m2_animation_load_requests: Dictionary = {} var _m2_animation_finalize_queue: Array = [] var _wmo_build_jobs: Dictionary = {} var _wmo_build_queue: Array = [] var _tile_loading_tasks: Dictionary = {} var _terrain_upgrade_tasks: Dictionary = {} var _terrain_control_splat_cache_tasks: Dictionary = {} var _terrain_splat_cache_tasks: Dictionary = {} var _terrain_splat_tasks: Dictionary = {} var _terrain_splat_result_mutex := Mutex.new() var _terrain_splat_result_queue: Array = [] var _water_load_queue: Array = [] var _water_load_queued: Dictionary = {} var _water_load_tasks: Dictionary = {} var _water_result_mutex := Mutex.new() var _water_result_queue: Array = [] var _tile_result_mutex := Mutex.new() var _tile_result_queue: Array = [] var _shared_tex_cache: Dictionary = {} var _terrain_root: Node3D var _update_accum := 0.0 var _tile_min := Vector2i(63, 63) var _tile_max := Vector2i(0, 0) var _camera_initialized := false var _editor_signature := "" var _streaming_focus: StreamingFocus var _missing_focus_source_reported := false var _last_focus_pos := Vector3.ZERO var _dbg_chunks_created := 0 var _dbg_chunks_null := 0 var _dbg_last_report := 0.0 var _runtime_stats_accum := 0.0 var _terrain_quality_log_accum := 0.0 var _shutting_down := false var _last_refresh_focus_pos := Vector3.ZERO var _has_refresh_focus := false var _tile_mesh_cache: Dictionary = {} var _tile_mesh_cache_order: Array = [] var _terrain_quality_mesh_cache: Dictionary = {} var _terrain_quality_mesh_cache_order: Array = [] var _wmo_prototype_cache: Dictionary = {} var _wmo_render_cache: Dictionary = {} var _wmo_render_missing_cache: Dictionary = {} var _wmo_render_load_requests: Dictionary = {} var _wmo_render_build_jobs: Dictionary = {} var _wmo_render_build_queue: Array = [] var _wmo_scene_resource_cache: Dictionary = {} var _wmo_scene_cache_missing: Dictionary = {} var _wmo_scene_load_requests: Dictionary = {} var _wmo_missing_cache: Dictionary = {} var _m2_scene_cache: Dictionary = {} var _m2_animated_scene_cache: Dictionary = {} var _m2_static_animation_cache: Dictionary = {} var _m2_missing_cache: Dictionary = {} var _world_wmo_root: Node3D var _wmo_registry: Dictionary = {} func _ready() -> void: _apply_quality_preset() if not ClassDB.class_exists("ADTLoader"): push_error("ADTLoader not found. Rebuild GDExtension first.") return if enable_occlusion_culling: get_viewport().set_use_occlusion_culling(true) _builder = ADT_BUILDER_SCRIPT.new() _map_dir = extracted_dir.path_join("World/Maps/%s" % map_name) _abs_map_dir = ProjectSettings.globalize_path(_map_dir) var existing_terrain := get_node_or_null("Terrain") if existing_terrain is Node3D: _terrain_root = existing_terrain else: _terrain_root = Node3D.new() _terrain_root.name = "Terrain" add_child(_terrain_root) _reset_terrain_root_children() _ensure_world_wmo_root() _set_editor_owner_recursive(_terrain_root) _scan_available_tiles() if _available_tiles.is_empty() and not _wdt_has_global_wmo(): push_error("No ADT tiles found in %s" % _map_dir) return if _can_use_baked_tile_cache(): var baked_dir := ProjectSettings.globalize_path(baked_tile_cache_dir.path_join(map_name)) if not DirAccess.dir_exists_absolute(baked_dir): push_warning("Baked terrain cache not found: %s. Runtime will fall back to raw ADT builds." % baked_dir) if terrain_control_splat_quality_enabled: var control_splat_dir := ProjectSettings.globalize_path(terrain_control_splat_cache_dir.path_join(map_name)) if not DirAccess.dir_exists_absolute(control_splat_dir): push_warning("Control splat terrain cache not found: %s. High terrain quality will use baked atlas fallback." % control_splat_dir) if terrain_splat_quality_enabled: var splat_dir := ProjectSettings.globalize_path(terrain_splat_cache_dir.path_join(map_name)) if not DirAccess.dir_exists_absolute(splat_dir): push_warning("Terrain splat cache not found: %s. High terrain quality will use baked atlas fallback." % splat_dir) if enable_wmo: _place_global_wmo() if Engine.is_editor_hint(): _tick_editor_streaming() elif auto_position_camera: _position_camera_over_world() if not Engine.is_editor_hint(): _capture_streaming_focus_from_source() set_process(true) call_deferred("_refresh_streaming_targets_after_ready") func _process(delta: float) -> void: if _available_tiles.is_empty(): return var profile_enabled := hitch_profiler_enabled and not Engine.is_editor_hint() var profile_start := Time.get_ticks_usec() if profile_enabled else 0 var section_start := profile_start var timings: Array[String] = [] # Queues are always drained — works in both editor and game. _drain_tile_load_results() _profile_section(timings, "finalize", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 _drain_terrain_upgrade_results() _profile_section(timings, "terrainup", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 _drain_terrain_control_splat_cache_results() _profile_section(timings, "terrainctl", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 _drain_terrain_splat_cache_results() _profile_section(timings, "terrainsplatcache", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 _drain_terrain_splat_results() _profile_section(timings, "terrainsplat", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 _process_water_load_queue() _drain_water_load_results() _profile_section(timings, "water", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 _process_queues() _profile_section(timings, "terrainq", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 _drain_m2_group_results() _profile_section(timings, "m2results", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 _drain_m2_animation_loads() _profile_section(timings, "m2anim", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 _drain_m2_mesh_loads() _profile_section(timings, "m2mesh", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 _process_m2_build_jobs() _profile_section(timings, "m2build", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 _process_wmo_build_jobs() _profile_section(timings, "wmobuild", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 _process_wmo_render_build_jobs() _profile_section(timings, "wmogroups", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 _process_detail_asset_queue() _profile_section(timings, "details", section_start, profile_enabled) section_start = Time.get_ticks_usec() if profile_enabled else 0 _tick_runtime_stats(delta) _tick_terrain_quality_log(delta) _update_accum += delta var did_refresh := false if _update_accum < update_interval: _log_hitch_profile(profile_start, timings, did_refresh, profile_enabled) return _update_accum = 0.0 if Engine.is_editor_hint(): _tick_editor_streaming() else: refresh_streaming_focus(false) did_refresh = true _profile_section(timings, "refresh", section_start, profile_enabled) _log_hitch_profile(profile_start, timings, did_refresh, profile_enabled) func _exit_tree() -> void: _shutting_down = true _wait_for_tile_tasks() _clear_streamed_world() _release_runtime_caches_for_shutdown() ## Frees detached prototype nodes and releases render resources owned by this ## loader after all asynchronous work has completed. Map refreshes retain these ## caches; only scene shutdown destroys them. func _release_runtime_caches_for_shutdown() -> void: _free_detached_node_cache(_m2_scene_cache) _free_detached_node_cache(_m2_animated_scene_cache) _free_detached_node_cache(_wmo_prototype_cache) _m2_mesh_cache.clear() _m2_static_animation_cache.clear() _m2_missing_cache.clear() _wmo_render_cache.clear() _wmo_scene_resource_cache.clear() _wmo_render_missing_cache.clear() _wmo_scene_cache_missing.clear() _wmo_missing_cache.clear() _shared_tex_cache.clear() func _free_detached_node_cache(node_cache: Dictionary) -> void: for cached_value in node_cache.values(): if not (cached_value is Node): continue var cached_node := cached_value as Node if not is_instance_valid(cached_node): continue if cached_node.is_inside_tree(): cached_node.queue_free() else: cached_node.free() node_cache.clear() func _apply_quality_preset() -> void: match quality_preset: QUALITY_PERFORMANCE: update_interval = 0.12 tiles_per_tick = 1 max_concurrent_tile_tasks = 1 chunk_ops_per_tick = 16 tile_lod_ops_per_tick = 1 tile_lod_remove_ops_per_tick = 1 tile_finalize_ops_per_tick = 1 terrain_upgrade_finalize_ops_per_tick = 1 max_concurrent_terrain_upgrade_tasks = 1 detail_asset_ops_per_tick = 1 m2_build_groups_per_tick = 1 m2_multimesh_batch_size = 32 m2_animation_finalize_ops_per_tick = 1 m2_mesh_finalize_ops_per_tick = 1 wmo_build_instances_per_tick = 1 wmo_render_group_ops_per_tick = 16 terrain_quality_mesh_cache_limit = 0 lod0_radius_chunks = 6 lod1_radius_chunks = 12 lod2_radius_chunks = 24 lod2_tile_radius = 3 warm_tile_margin = 1 prewarm_tile_margin = 2 retain_tile_margin = 3 full_lod_prewarm_tiles = 1.0 boundary_prefetch_threshold = 0.4 terrain_full_quality_enabled = false terrain_full_quality_radius_tiles = 0 terrain_control_splat_quality_enabled = false terrain_control_splat_primary = false terrain_control_splat_hide_fallback = false terrain_control_splat_hide_fallback_radius_tiles = 0 terrain_control_splat_radius_tiles = 0 max_active_terrain_control_splat_tiles = 0 terrain_control_splat_cache_finalize_ops_per_tick = 1 terrain_splat_quality_enabled = false terrain_splat_radius_tiles = 0 terrain_splat_lod = 3 terrain_splat_runtime_build_enabled = false max_active_terrain_splat_tiles = 0 terrain_splat_cache_finalize_ops_per_tick = 1 max_concurrent_terrain_splat_tasks = 1 terrain_splat_builds_per_tick = 1 m2_tile_radius = 1 wmo_tile_radius = 3 m2_visibility_range = 700.0 wmo_visibility_range = 1800.0 m2_cast_shadows = false wmo_cast_shadows = false terrain_cast_shadows = false QUALITY_BALANCED: update_interval = 0.10 tiles_per_tick = 1 max_concurrent_tile_tasks = 1 chunk_ops_per_tick = 20 tile_lod_ops_per_tick = 1 tile_lod_remove_ops_per_tick = 1 tile_finalize_ops_per_tick = 1 terrain_upgrade_finalize_ops_per_tick = 1 max_concurrent_terrain_upgrade_tasks = 1 detail_asset_ops_per_tick = 1 m2_build_groups_per_tick = 1 m2_multimesh_batch_size = 32 m2_animation_finalize_ops_per_tick = 1 m2_mesh_finalize_ops_per_tick = 1 wmo_build_instances_per_tick = 1 wmo_render_group_ops_per_tick = 20 terrain_quality_mesh_cache_limit = 24 lod0_radius_chunks = 8 lod1_radius_chunks = 16 lod2_radius_chunks = 32 lod2_tile_radius = 4 warm_tile_margin = 1 prewarm_tile_margin = 2 retain_tile_margin = 3 full_lod_prewarm_tiles = 1.0 boundary_prefetch_threshold = 0.4 terrain_full_quality_enabled = true terrain_full_quality_radius_tiles = 1 terrain_control_splat_quality_enabled = true terrain_control_splat_primary = true terrain_control_splat_hide_fallback = false terrain_control_splat_hide_fallback_radius_tiles = 0 terrain_control_splat_radius_tiles = 0 max_active_terrain_control_splat_tiles = 1 terrain_control_splat_cache_finalize_ops_per_tick = 1 terrain_splat_quality_enabled = false terrain_splat_radius_tiles = 0 terrain_splat_lod = 0 terrain_splat_runtime_build_enabled = false max_active_terrain_splat_tiles = 1 terrain_splat_cache_finalize_ops_per_tick = 1 max_concurrent_terrain_splat_tasks = 1 terrain_splat_builds_per_tick = 1 m2_tile_radius = 2 wmo_tile_radius = 4 m2_visibility_range = 1100.0 wmo_visibility_range = 2600.0 m2_cast_shadows = false wmo_cast_shadows = false terrain_cast_shadows = false QUALITY_HIGH: update_interval = 0.10 tiles_per_tick = 1 max_concurrent_tile_tasks = 1 chunk_ops_per_tick = 24 tile_lod_ops_per_tick = 1 tile_lod_remove_ops_per_tick = 1 tile_finalize_ops_per_tick = 1 terrain_upgrade_finalize_ops_per_tick = 1 max_concurrent_terrain_upgrade_tasks = 1 detail_asset_ops_per_tick = 1 m2_build_groups_per_tick = 1 m2_multimesh_batch_size = 64 m2_animation_finalize_ops_per_tick = 1 m2_mesh_finalize_ops_per_tick = 1 wmo_build_instances_per_tick = 1 wmo_render_group_ops_per_tick = 16 terrain_quality_mesh_cache_limit = 48 lod0_radius_chunks = 10 lod1_radius_chunks = 20 lod2_radius_chunks = 40 lod2_tile_radius = 5 warm_tile_margin = 1 prewarm_tile_margin = 3 retain_tile_margin = 4 full_lod_prewarm_tiles = 1.5 boundary_prefetch_threshold = 0.42 terrain_full_quality_enabled = true terrain_full_quality_radius_tiles = 5 terrain_control_splat_quality_enabled = true terrain_control_splat_primary = true terrain_control_splat_hide_fallback = true terrain_control_splat_hide_fallback_radius_tiles = 0 terrain_control_splat_radius_tiles = 5 max_active_terrain_control_splat_tiles = 121 terrain_control_splat_cache_finalize_ops_per_tick = 8 terrain_splat_quality_enabled = false terrain_splat_radius_tiles = 0 terrain_splat_lod = 0 terrain_splat_runtime_build_enabled = false max_active_terrain_splat_tiles = 5 terrain_splat_cache_finalize_ops_per_tick = 1 max_concurrent_terrain_splat_tasks = 1 terrain_splat_builds_per_tick = 1 m2_tile_radius = 3 wmo_tile_radius = 5 m2_visibility_range = 1600.0 wmo_visibility_range = 3600.0 m2_cast_shadows = false wmo_cast_shadows = false terrain_cast_shadows = false func _tick_editor_streaming() -> void: if editor_reload_now: editor_reload_now = false _clear_streamed_world() _editor_signature = "" if not editor_preview_enabled: if not _editor_signature.is_empty(): _clear_streamed_world() _terrain_root.position = Vector3.ZERO _editor_signature = "" return var sig := _make_editor_signature() if sig != _editor_signature: _editor_signature = sig _clear_streamed_world() _reset_refresh_focus() # Offset terrain_root so preview center sits near Godot origin. var focus := _tile_center_to_world(editor_preview_center_x, editor_preview_center_y) _terrain_root.position = -focus var focus_world := _tile_center_to_world(editor_preview_center_x, editor_preview_center_y) if editor_follow_view_camera: var editor_camera := _get_editor_view_camera() if editor_camera: focus_world += editor_camera.global_position _set_streaming_focus_from_vector3(focus_world) _refresh_editor_streaming_targets_at(_streaming_focus_to_vector3(), false) func _scan_available_tiles() -> void: _available_tiles.clear() _wdt_info = {} if _load_tiles_from_wdt(): print("StreamingWorld: loaded %d ADT tiles from WDT for %s" % [_available_tiles.size(), map_name]) return push_warning("StreamingWorld: falling back to directory scan for %s (no WDT)" % map_name) _load_tiles_from_directory() print("StreamingWorld: found %d ADT tiles by directory scan for %s" % [_available_tiles.size(), map_name]) func _load_tiles_from_wdt() -> bool: if not ClassDB.class_exists("WDTLoader"): return false var wdt_path_abs := _abs_map_dir.path_join("%s.wdt" % map_name) if not FileAccess.file_exists(wdt_path_abs): return false var loader = ClassDB.instantiate("WDTLoader") if loader == null: return false var data: Dictionary = loader.call("load_wdt", wdt_path_abs) if data.is_empty(): push_warning("StreamingWorld: WDTLoader returned empty data for %s" % wdt_path_abs) return false var tiles: Array = data.get("tiles", []) for tile_variant in tiles: if not (tile_variant is Dictionary): continue var entry: Dictionary = tile_variant var tx: int = int(entry.get("x", -1)) var ty: int = int(entry.get("y", -1)) if tx < 0 or tx > 63 or ty < 0 or ty > 63: continue var adt_file := "%s_%d_%d.adt" % [map_name, tx, ty] var adt_abs := _abs_map_dir.path_join(adt_file) # WDT flags the tile as present, but the ADT may still be missing in # a partial extraction. Skip so downstream code never chases a ghost. if not FileAccess.file_exists(adt_abs): continue var key := _tile_key(tx, ty) _available_tiles[key] = adt_abs _tile_min.x = min(_tile_min.x, tx) _tile_min.y = min(_tile_min.y, ty) _tile_max.x = max(_tile_max.x, tx) _tile_max.y = max(_tile_max.y, ty) _wdt_info = data return not _available_tiles.is_empty() func _load_tiles_from_directory() -> void: var dir := DirAccess.open(_map_dir) if dir == null: push_error("Cannot open map directory: %s" % _map_dir) return for file_name in dir.get_files(): if not file_name.ends_with(".adt"): continue var stem := file_name.trim_suffix(".adt") var parts := stem.split("_") if parts.size() != 3 or parts[0] != map_name: continue if not parts[1].is_valid_int() or not parts[2].is_valid_int(): continue var tx := parts[1].to_int() var ty := parts[2].to_int() var key := _tile_key(tx, ty) _available_tiles[key] = _abs_map_dir.path_join(file_name) _tile_min.x = min(_tile_min.x, tx) _tile_min.y = min(_tile_min.y, ty) _tile_max.x = max(_tile_max.x, tx) _tile_max.y = max(_tile_max.y, ty) ## Replaces the current streaming point without requiring a Node or Camera3D. func set_streaming_focus(streaming_focus: StreamingFocus) -> void: _streaming_focus = streaming_focus ## Samples the configured scene source, then refreshes streaming from the typed focus. ## Returns false when no valid focus is available; existing streamed content is retained. func refresh_streaming_focus(force: bool = false) -> bool: _capture_streaming_focus_from_source() if _streaming_focus == null or _streaming_focus.world_position == null: return false _terrain_root.position = Vector3.ZERO _refresh_streaming_targets_at(_streaming_focus_to_vector3(), force) return true func _refresh_streaming_targets_after_ready() -> void: _has_refresh_focus = false refresh_streaming_focus(false) ## Core streaming update for runtime and editor focus positions. func _refresh_editor_streaming_targets_at(focus_pos: Vector3, force: bool) -> void: if not force and not _should_refresh_focus(focus_pos): return var focus_tile := _world_to_tile(focus_pos) var total_tile_radius: int = maxi(0, editor_preview_tile_radius) var wanted_tiles: Dictionary = {} for ty in range(focus_tile.y - total_tile_radius, focus_tile.y + total_tile_radius + 1): for tx in range(focus_tile.x - total_tile_radius, focus_tile.x + total_tile_radius + 1): var key := _tile_key(tx, ty) if _available_tiles.has(key): wanted_tiles[key] = true _apply_streaming_target(wanted_tiles, wanted_tiles, focus_pos) _note_refresh_focus(focus_pos) if force or debug_streaming: print("StreamingWorld(editor): focus_tile=%s wanted=%d queued=%d" % [ focus_tile, wanted_tiles.size(), _tile_load_queue.size()]) func _refresh_streaming_targets_at(focus_pos: Vector3, force: bool) -> void: if not force and not _should_refresh_focus(focus_pos): return var focus_tile := _world_to_tile(focus_pos) # Chunk-LOD radius → tile radius (ceiling), plus margin for pre-warming. var focus_tiles := _predictive_focus_tiles(focus_pos) var total_tile_radius: int = _runtime_load_tile_radius() var retain_tile_radius: int = _runtime_retain_tile_radius() var retained_tiles: Dictionary = {} for center in focus_tiles: for ty in range(center.y - retain_tile_radius, center.y + retain_tile_radius + 1): for tx in range(center.x - retain_tile_radius, center.x + retain_tile_radius + 1): var retained_key := _tile_key(tx, ty) if _available_tiles.has(retained_key): retained_tiles[retained_key] = true var wanted_tiles: Dictionary = {} for center in focus_tiles: for ty in range(center.y - total_tile_radius, center.y + total_tile_radius + 1): for tx in range(center.x - total_tile_radius, center.x + total_tile_radius + 1): var key := _tile_key(tx, ty) if _available_tiles.has(key): wanted_tiles[key] = true _apply_streaming_target(wanted_tiles, retained_tiles, focus_pos) _note_refresh_focus(focus_pos) if force or debug_streaming: print("StreamingWorld: focus_tile=%s wanted=%d queued=%d" % [ focus_tile, wanted_tiles.size(), _tile_load_queue.size()]) func _apply_streaming_target(wanted_tiles: Dictionary, retained_tiles: Dictionary, focus_pos: Vector3) -> void: _last_focus_pos = focus_pos var filtered_load_queue: Array = [] for request in _tile_load_queue: if wanted_tiles.has(request["key"]) and not _tile_states.has(request["key"]): filtered_load_queue.append(request) _tile_load_queue = filtered_load_queue for key in wanted_tiles.keys(): if not _tile_states.has(key) and not _is_tile_queued(key): var parts: PackedStringArray = key.split("_") var tx: int = int(parts[0]) var ty: int = int(parts[1]) var tcx: float = (tx + 0.5) * TILE_SIZE var tcz: float = (ty + 0.5) * TILE_SIZE var dx: float = tcx - focus_pos.x var dz: float = tcz - focus_pos.z _tile_load_queue.append({ "key": key, "tx": tx, "ty": ty, "path": _available_tiles[key], "dist_sq": dx * dx + dz * dz, }) # Nearest tiles first _tile_load_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["dist_sq"] < b["dist_sq"]) for key in _tile_states.keys(): var state: Dictionary = _tile_states[key] var wanted := wanted_tiles.has(key) var retained := retained_tiles.has(key) state["wanted"] = wanted state["retained"] = retained if wanted: state["desired_lods"] = _compute_desired_lods(state, focus_pos) state["desired_tile_lod"] = -1 if not state["desired_lods"].is_empty() else _compute_desired_tile_lod(state, focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos) elif retained: state["desired_lods"] = {} var current_tile_lod: int = int(state.get("tile_lod", -1)) state["desired_tile_lod"] = current_tile_lod if current_tile_lod >= 0 else -1 state["desired_tile_lod_visible"] = false else: state["desired_lods"] = {} state["desired_tile_lod"] = -1 state["desired_tile_lod_visible"] = false state = _sync_terrain_quality(state, focus_pos) state = _sync_detail_assets(state, focus_pos) _tile_states[key] = state _prune_active_terrain_control_splat_tiles(focus_pos) _prune_active_terrain_splat_tiles(focus_pos) _rebuild_chunk_queues() func _chunk_origin_for(state: Dictionary, chunk_id: int) -> Vector3: var chunks: Array = state["data"].get("chunks", []) if chunk_id >= 0 and chunk_id < chunks.size(): var c: Dictionary = chunks[chunk_id] if not c.is_empty(): return c.get("origin", Vector3.ZERO) return state.get("origin", Vector3.ZERO) func _rebuild_chunk_queues() -> void: _chunk_remove_queue.clear() _chunk_create_queue.clear() _tile_lod_remove_queue.clear() _tile_lod_create_queue.clear() for key in _tile_states.keys(): var state: Dictionary = _tile_states[key] var current_chunks: Dictionary = state["chunks"] var desired_lods: Dictionary = state["desired_lods"] var current_tile_lod: int = int(state.get("tile_lod", -1)) var desired_tile_lod: int = int(state.get("desired_tile_lod", -1)) var current_tile_lod_visible: bool = bool(state.get("tile_lod_visible", false)) var desired_tile_lod_visible: bool = bool(state.get("desired_tile_lod_visible", false)) if desired_tile_lod >= 0: for chunk_id in current_chunks.keys(): _chunk_remove_queue.append({"tile": key, "chunk": chunk_id}) if current_tile_lod != desired_tile_lod or current_tile_lod_visible != desired_tile_lod_visible: _tile_lod_create_queue.append({ "tile": key, "lod": desired_tile_lod, "visible": desired_tile_lod_visible, "dist_sq": _tile_dist_sq(state, _last_focus_pos), }) elif _should_prepare_full_tile_lod(state, _last_focus_pos): _tile_lod_create_queue.append({ "tile": key, "lod": 0, "visible": false, "prepare": true, "dist_sq": _tile_dist_sq(state, _last_focus_pos), }) continue if current_tile_lod >= 0 and _is_tile_chunk_set_ready(state): _tile_lod_remove_queue.append({"tile": key}) for chunk_id in current_chunks.keys(): if not desired_lods.has(chunk_id): _chunk_remove_queue.append({"tile": key, "chunk": chunk_id}) continue if current_chunks[chunk_id]["lod"] != desired_lods[chunk_id]: var lod_change_origin: Vector3 = _chunk_origin_for(state, chunk_id) var dx2: float = lod_change_origin.x - _last_focus_pos.x var dz2: float = lod_change_origin.z - _last_focus_pos.z _chunk_create_queue.append({ "tile": key, "chunk": chunk_id, "lod": desired_lods[chunk_id], "dist_sq": dx2 * dx2 + dz2 * dz2, }) for chunk_id in desired_lods.keys(): if current_chunks.has(chunk_id): continue var chunk_origin: Vector3 = _chunk_origin_for(state, chunk_id) var dx: float = chunk_origin.x - _last_focus_pos.x var dz: float = chunk_origin.z - _last_focus_pos.z _chunk_create_queue.append({ "tile": key, "chunk": chunk_id, "lod": desired_lods[chunk_id], "dist_sq": dx * dx + dz * dz, }) # Sort after fully rebuilding so nearest chunks are processed first _chunk_create_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["dist_sq"] < b["dist_sq"]) _tile_lod_create_queue.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a["dist_sq"] < b["dist_sq"]) func _process_queues() -> void: var ops_left: int = chunk_ops_per_tick var tile_lod_create_left: int = tile_lod_ops_per_tick var tile_lod_remove_left: int = tile_lod_remove_ops_per_tick # Free stale terrain first so GPU descriptors/memory are reclaimed before new creates. while ops_left > 0 and not _chunk_remove_queue.is_empty(): _remove_chunk(_chunk_remove_queue.pop_back()) ops_left -= 1 while tile_lod_remove_left > 0 and not _tile_lod_remove_queue.is_empty(): _remove_tile_lod(_tile_lod_remove_queue.pop_back()) tile_lod_remove_left -= 1 # Parse N ADT files this tick — the main throttle knob var loads_left: int = tiles_per_tick while loads_left > 0 and not _tile_load_queue.is_empty() and _tile_loading_tasks.size() < max_concurrent_tile_tasks: _start_tile_load_async(_tile_load_queue.pop_front()) loads_left -= 1 while tile_lod_create_left > 0 and not _tile_lod_create_queue.is_empty(): _create_tile_lod(_tile_lod_create_queue.pop_front()) tile_lod_create_left -= 1 # Create chunk meshes — sorted nearest-first so visible detail appears first while ops_left > 0 and not _chunk_create_queue.is_empty(): _create_chunk(_chunk_create_queue.pop_front()) ops_left -= 1 # Debug summary every 2 seconds _dbg_last_report += get_process_delta_time() if debug_streaming and _dbg_last_report >= 2.0: _dbg_last_report = 0.0 print("DBG QUEUES tiles_load=%d tile_lod_create=%d tile_lod_remove=%d chunk_create=%d chunk_remove=%d created=%d null=%d states=%d" % [ _tile_load_queue.size(), _tile_lod_create_queue.size(), _tile_lod_remove_queue.size(), _chunk_create_queue.size(), _chunk_remove_queue.size(), _dbg_chunks_created, _dbg_chunks_null, _tile_states.size()]) func _profile_section(timings: Array[String], label: String, start_usec: int, enabled: bool) -> void: if not enabled: return var elapsed_ms := float(Time.get_ticks_usec() - start_usec) / 1000.0 if elapsed_ms >= 0.05: timings.append("%s=%.2f" % [label, elapsed_ms]) func _log_hitch_profile(start_usec: int, timings: Array[String], did_refresh: bool, enabled: bool) -> void: if not enabled: return var total_ms := float(Time.get_ticks_usec() - start_usec) / 1000.0 if total_ms < maxf(1.0, hitch_profiler_threshold_ms): return print("HITCH %.2fms refresh=%s queues[tile=%d loading=%d terrainup=%d terrainctl=%d terrainsplat=%d water=%d detail=%d m2task=%d m2anim=%d m2mesh=%d m2build=%d wmobuild=%d wmogroups=%d lod+=%d lod-=%d chunk+=%d chunk-=%d] %s" % [ total_ms, str(did_refresh), _tile_load_queue.size(), _tile_loading_tasks.size(), _terrain_upgrade_tasks.size(), _terrain_control_splat_cache_tasks.size(), _terrain_splat_queue_size(), _water_load_queue.size() + _water_load_tasks.size(), _detail_asset_queue.size(), _m2_group_tasks.size(), _m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(), _m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(), _m2_build_queue.size(), _wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(), _wmo_render_build_queue.size(), _tile_lod_create_queue.size(), _tile_lod_remove_queue.size(), _chunk_create_queue.size(), _chunk_remove_queue.size(), " ".join(timings), ]) ## Read-only machine-readable state used by renderer baseline tooling. ## Keep this free of queue mutation so checkpoint capture cannot alter runtime behavior. func render_baseline_snapshot() -> Dictionary: return { "tiles": _tile_states.size(), "loading": _tile_loading_tasks.size(), "queues": { "tile": _tile_load_queue.size(), "terrain_upgrade": _terrain_upgrade_tasks.size(), "terrain_control": _terrain_control_splat_cache_tasks.size(), "terrain_splat": _terrain_splat_queue_size(), "water": _water_load_queue.size() + _water_load_tasks.size(), "detail": _detail_asset_queue.size(), "m2_task": _m2_group_tasks.size(), "m2_animation": _m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(), "m2_mesh": _m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(), "m2_build": _m2_build_queue.size(), "wmo_build": _wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(), "wmo_groups": _wmo_render_build_queue.size(), "lod_create": _tile_lod_create_queue.size(), "lod_remove": _tile_lod_remove_queue.size(), "chunk_create": _chunk_create_queue.size(), "chunk_remove": _chunk_remove_queue.size(), }, "cache_entries": _tile_mesh_cache.size(), "m2_active_unique_ids": _m2_unique_registry.size(), "wmo_instances": _wmo_registry.size(), } func _tick_runtime_stats(delta: float) -> void: if not runtime_stats_enabled or Engine.is_editor_hint(): return _runtime_stats_accum += delta if _runtime_stats_accum < maxf(runtime_stats_interval, 0.25): return _runtime_stats_accum = 0.0 var wanted_tiles := 0 var retained_tiles := 0 var chunk_nodes := 0 var chunk_rids := 0 var tile_lod_nodes := 0 var tile_lod_rids := 0 var tile_lod_hidden := 0 var tile_lod_prepared := 0 var m2_tiles := 0 var m2_multimeshes := 0 var m2_instances := 0 var terrain_baked_full := 0 var terrain_control_splat := 0 var terrain_splat := 0 for state_variant in _tile_states.values(): if not (state_variant is Dictionary): continue var state: Dictionary = state_variant if bool(state.get("wanted", false)): wanted_tiles += 1 if bool(state.get("retained", false)): retained_tiles += 1 var chunks: Dictionary = state.get("chunks", {}) for entry_variant in chunks.values(): if not (entry_variant is Dictionary): continue var entry: Dictionary = entry_variant if bool(entry.get("empty", false)): continue var chunk_rid: RID = entry.get("rid", RID()) if chunk_rid.is_valid(): chunk_rids += 1 elif entry.get("node", null) != null: chunk_nodes += 1 var tile_lod_rid: RID = state.get("tile_lod_rid", RID()) if tile_lod_rid.is_valid(): tile_lod_rids += 1 elif state.get("tile_lod_node", null) != null: tile_lod_nodes += 1 if int(state.get("tile_lod", -1)) >= 0 and not bool(state.get("tile_lod_visible", true)): tile_lod_hidden += 1 var prepared_tile_lod_rid: RID = state.get("prepared_tile_lod_rid", RID()) if prepared_tile_lod_rid.is_valid(): tile_lod_prepared += 1 var terrain_source := String(state.get("quality_terrain_source", "")) if terrain_source == "baked_full": terrain_baked_full += 1 elif terrain_source == "control_splat_cache": terrain_control_splat += 1 elif terrain_source == "splat_cache" or terrain_source == "splat": terrain_splat += 1 var root: Node = state.get("root", null) if root == null or not is_instance_valid(root): continue var m2_root := root.get_node_or_null("M2s") if m2_root == null: continue m2_tiles += 1 var m2_counts := {"multimeshes": 0, "instances": 0} _accumulate_multimesh_stats(m2_root, m2_counts) m2_multimeshes += int(m2_counts["multimeshes"]) m2_instances += int(m2_counts["instances"]) print("PERF fps=%.1f tiles=%d wanted=%d retained=%d loading=%d terrainup=%d terrainctl=%d terrainsplat=%d water=%d src[baked=%d ctl=%d splat=%d] cache=%d lod_rid=%d lod_node=%d lod_hidden=%d lod_prepared=%d chunk_rid=%d chunk_node=%d queues[tile=%d detail=%d m2_task=%d m2_anim=%d m2_mesh=%d m2_build=%d wmo_build=%d wmo_groups=%d lod+=%d lod-=%d chunk+=%d chunk-=%d] m2_tiles=%d m2_mm=%d m2_inst=%d wmo=%d" % [ Engine.get_frames_per_second(), _tile_states.size(), wanted_tiles, retained_tiles, _tile_loading_tasks.size(), _terrain_upgrade_tasks.size(), _terrain_control_splat_cache_tasks.size(), _terrain_splat_queue_size(), _water_load_queue.size() + _water_load_tasks.size(), terrain_baked_full, terrain_control_splat, terrain_splat, _tile_mesh_cache.size(), tile_lod_rids, tile_lod_nodes, tile_lod_hidden, tile_lod_prepared, chunk_rids, chunk_nodes, _tile_load_queue.size(), _detail_asset_queue.size(), _m2_group_tasks.size(), _m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(), _m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(), _m2_build_queue.size(), _wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(), _wmo_render_build_queue.size(), _tile_lod_create_queue.size(), _tile_lod_remove_queue.size(), _chunk_create_queue.size(), _chunk_remove_queue.size(), m2_tiles, m2_multimeshes, m2_instances, _wmo_registry.size(), ]) func _tick_terrain_quality_log(delta: float) -> void: if not terrain_quality_log_enabled or Engine.is_editor_hint(): return _terrain_quality_log_accum += delta if _terrain_quality_log_accum < maxf(terrain_quality_log_interval, 0.25): return _terrain_quality_log_accum = 0.0 var target := 0 var ready := 0 var pending := 0 var fallback_baked := 0 var fallback_stream := 0 var hidden_wait := 0 var missing_cache := 0 var stale_or_other := 0 var focus_state_source := "none" var focus_tile := _world_to_tile(_last_focus_pos) for state_variant in _tile_states.values(): if not (state_variant is Dictionary): continue var state: Dictionary = state_variant if not bool(state.get("wanted", false)): continue if not terrain_control_splat_quality_enabled: continue if not _tile_within_control_splat_terrain_radius(state, _last_focus_pos): continue target += 1 var key := String(state.get("key", "")) var source := String(state.get("quality_terrain_source", "")) var is_focus := int(state.get("tx", -9999)) == focus_tile.x and int(state.get("ty", -9999)) == focus_tile.y if is_focus: focus_state_source = source if not source.is_empty() else "stream" if source == "control_splat_cache": ready += 1 continue if not key.is_empty() and _terrain_control_splat_cache_tasks.has(key): pending += 1 continue var cache_path := _get_terrain_control_splat_resource_path(int(state.get("tx", 0)), int(state.get("ty", 0))) if not ResourceLoader.exists(cache_path): missing_cache += 1 continue var desired_lod := int(state.get("desired_tile_lod", -1)) if desired_lod < 0: hidden_wait += 1 elif source == "baked_full": fallback_baked += 1 else: var tile_lod := int(state.get("tile_lod", -1)) if tile_lod >= 0: fallback_stream += 1 else: stale_or_other += 1 var done := ready >= target and pending == 0 and missing_cache == 0 and hidden_wait == 0 and fallback_baked == 0 and fallback_stream == 0 print("TERRAIN_QUALITY target=%d ready=%d pending=%d fallback[baked=%d stream=%d hidden=%d other=%d] missing_cache=%d queue=%d focus=%d_%d:%s done=%s" % [ target, ready, pending, fallback_baked, fallback_stream, hidden_wait, stale_or_other, missing_cache, _terrain_control_splat_cache_tasks.size(), focus_tile.x, focus_tile.y, focus_state_source, str(done), ]) func _accumulate_multimesh_stats(node: Node, counts: Dictionary) -> void: if node is MultiMeshInstance3D: var mmi := node as MultiMeshInstance3D if mmi.multimesh: counts["multimeshes"] = int(counts["multimeshes"]) + 1 counts["instances"] = int(counts["instances"]) + mmi.multimesh.instance_count for child in node.get_children(): _accumulate_multimesh_stats(child, counts) func _start_tile_load_async(request: Dictionary) -> void: var key: String = request["key"] if _tile_loading_tasks.has(key): return var stream_res_path := _get_streaming_tile_resource_path(int(request["tx"]), int(request["ty"])) if _can_use_baked_tile_cache() and ResourceLoader.exists(stream_res_path): var err := ResourceLoader.load_threaded_request( stream_res_path, "", false, ResourceLoader.CACHE_MODE_REUSE) if err == OK: _tile_loading_tasks[key] = { "mode": "stream", "path": stream_res_path, "request": request, } return var baked_res_path := _get_baked_tile_resource_path(int(request["tx"]), int(request["ty"])) if _can_use_baked_tile_cache() and ResourceLoader.exists(baked_res_path): var err := ResourceLoader.load_threaded_request( baked_res_path, "", false, ResourceLoader.CACHE_MODE_REUSE) if err == OK: _tile_loading_tasks[key] = { "mode": "baked", "path": baked_res_path, "request": request, } return _start_raw_tile_load_async(request) func _start_raw_tile_load_async(request: Dictionary) -> void: var key: String = request["key"] if _tile_loading_tasks.has(key): return var task_id: int = WorkerThreadPool.add_task(_load_tile_task.bind(request)) _tile_loading_tasks[key] = { "mode": "raw", "task_id": task_id, "request": request, } func _load_tile_task(request: Dictionary) -> void: var data: Dictionary = {} var loader = ClassDB.instantiate("ADTLoader") if loader: data = loader.call("load_adt", request["path"]) _tile_result_mutex.lock() _tile_result_queue.append({ "key": request["key"], "data": data, }) _tile_result_mutex.unlock() func _drain_tile_load_results() -> void: var results: Array = [] var finalize_budget: int = maxi(1, tile_finalize_ops_per_tick) _tile_result_mutex.lock() while finalize_budget > 0 and not _tile_result_queue.is_empty(): results.append(_tile_result_queue.pop_front()) finalize_budget -= 1 _tile_result_mutex.unlock() for result in results: var key: String = result["key"] if not _tile_loading_tasks.has(key): continue var pending: Dictionary = _tile_loading_tasks[key] if pending.get("mode", "raw") != "raw": continue WorkerThreadPool.wait_for_task_completion(int(pending["task_id"])) _tile_loading_tasks.erase(key) if _shutting_down: continue var request: Dictionary = pending["request"] if not _should_accept_loaded_tile(request): continue _finalize_loaded_tile(request, result["data"]) var baked_ready: Array = [] if finalize_budget <= 0: return for key in _tile_loading_tasks.keys(): if baked_ready.size() >= finalize_budget: break var pending: Dictionary = _tile_loading_tasks[key] if pending.get("mode", "raw") != "baked" and pending.get("mode", "raw") != "stream": continue var status := ResourceLoader.load_threaded_get_status(pending["path"]) if status == ResourceLoader.THREAD_LOAD_LOADED or status == ResourceLoader.THREAD_LOAD_FAILED: baked_ready.append(key) for key in baked_ready: if not _tile_loading_tasks.has(key): continue var pending: Dictionary = _tile_loading_tasks[key] _tile_loading_tasks.erase(key) var request: Dictionary = pending["request"] if _shutting_down: continue var status := ResourceLoader.load_threaded_get_status(pending["path"]) if status != ResourceLoader.THREAD_LOAD_LOADED: _start_raw_tile_load_async(request) continue var loaded_tile: Resource = ResourceLoader.load_threaded_get(pending["path"]) if not _should_accept_loaded_tile(request): continue if pending.get("mode", "raw") == "stream": if loaded_tile == null or loaded_tile.get_script() != STREAMING_TILE_SCRIPT: push_warning("Invalid streaming tile resource: %s" % pending["path"]) _start_raw_tile_load_async(request) continue _finalize_loaded_streaming_tile(request, loaded_tile) continue if loaded_tile == null or loaded_tile.get_script() != BAKED_TILE_SCRIPT: push_warning("Invalid baked tile resource: %s" % pending["path"]) _start_raw_tile_load_async(request) continue _finalize_loaded_baked_tile(request, loaded_tile) func _sync_terrain_quality(state: Dictionary, focus_pos: Vector3) -> Dictionary: state = _drop_splat_quality_if_inactive(state, focus_pos) if not terrain_full_quality_enabled: return state if state.get("stream_tile", null) == null: return state if not bool(state.get("wanted", false)): return state if not _tile_within_full_terrain_radius(state, focus_pos): return state var key := String(state.get("key", "")) var quality_mesh: Mesh = state.get("quality_terrain_mesh", null) var quality_source := String(state.get("quality_terrain_source", "")) var control_splat_requested := false if terrain_control_splat_quality_enabled and _tile_within_control_splat_terrain_radius(state, focus_pos): if quality_source == "control_splat_cache": return state if key.is_empty(): return state if not _can_activate_terrain_control_splat(key, state, focus_pos): if quality_mesh != null: return state return state if terrain_control_splat_primary and _tile_is_focus_tile(state, focus_pos): var immediate_state := _load_terrain_control_splat_immediate(state, focus_pos) if String(immediate_state.get("quality_terrain_source", "")) == "control_splat_cache": return immediate_state control_splat_requested = _request_terrain_control_splat_cache(state) if control_splat_requested: return state if state.get("baked_tile", null) != null: return state if terrain_splat_quality_enabled and _tile_within_splat_terrain_radius(state, focus_pos): if quality_source == "splat_cache" or quality_source == "splat": return state if key.is_empty(): return state if not _can_activate_terrain_splat(key, state, focus_pos): if quality_mesh != null: return state return state if _request_terrain_splat_cache(state): return state if terrain_splat_runtime_build_enabled: if _terrain_splat_tasks.has(key): return state if _terrain_splat_tasks.size() >= maxi(1, max_concurrent_terrain_splat_tasks): return state var task_id: int = WorkerThreadPool.add_task(_load_terrain_splat_task.bind(key, String(state.get("path", "")))) _terrain_splat_tasks[key] = task_id return state if quality_mesh != null: return state if terrain_control_splat_primary and control_splat_requested: return state if terrain_control_splat_primary and terrain_control_splat_quality_enabled and _tile_within_control_splat_terrain_radius(state, focus_pos): var control_path := _get_terrain_control_splat_resource_path(int(state.get("tx", 0)), int(state.get("ty", 0))) if ResourceLoader.exists(control_path): return state if key.is_empty() or _terrain_upgrade_tasks.has(key): return state if _terrain_upgrade_tasks.size() >= maxi(1, max_concurrent_terrain_upgrade_tasks): return state var baked_path := _get_baked_tile_resource_path(int(state.get("tx", 0)), int(state.get("ty", 0))) if not ResourceLoader.exists(baked_path): return state var err := ResourceLoader.load_threaded_request( baked_path, "", false, ResourceLoader.CACHE_MODE_IGNORE) if err == OK or err == ERR_BUSY: _terrain_upgrade_tasks[key] = { "path": baked_path, "tx": int(state.get("tx", 0)), "ty": int(state.get("ty", 0)), } return state func _drop_splat_quality_if_inactive(state: Dictionary, focus_pos: Vector3) -> Dictionary: var quality_source := String(state.get("quality_terrain_source", "")) if quality_source == "control_splat_cache": if bool(state.get("wanted", false)) and _tile_within_control_splat_terrain_radius(state, focus_pos): return state return _drop_splat_quality(state) if quality_source != "splat_cache" and quality_source != "splat": return state if bool(state.get("wanted", false)) and _tile_within_splat_terrain_radius(state, focus_pos): return state return _drop_splat_quality(state) func _drop_splat_quality(state: Dictionary) -> Dictionary: state.erase("quality_terrain_mesh") state.erase("quality_terrain_source") var key := String(state.get("key", "")) if not key.is_empty(): state = _restore_quality_terrain_mesh(key, state) _remove_tile_mesh_cache_entries(key) return state func _can_activate_terrain_splat(key: String, state: Dictionary, focus_pos: Vector3) -> bool: var limit := maxi(0, max_active_terrain_splat_tiles) if limit <= 0: return false if _tile_is_focus_tile(state, focus_pos): return true if _terrain_splat_cache_tasks.has(key) or _terrain_splat_tasks.has(key): return true return _active_terrain_splat_count(focus_pos) < limit func _can_activate_terrain_control_splat(key: String, state: Dictionary, focus_pos: Vector3) -> bool: var limit := maxi(0, max_active_terrain_control_splat_tiles) if limit <= 0: return false if _tile_is_focus_tile(state, focus_pos): return true if _terrain_control_splat_cache_tasks.has(key): return true return _active_terrain_control_splat_count(focus_pos) < limit func _active_terrain_control_splat_count(focus_pos: Vector3) -> int: var count := _terrain_control_splat_cache_tasks.size() for state_variant in _tile_states.values(): if not (state_variant is Dictionary): continue var state: Dictionary = state_variant if not bool(state.get("wanted", false)): continue if String(state.get("quality_terrain_source", "")) != "control_splat_cache": continue if _tile_within_control_splat_terrain_radius(state, focus_pos): count += 1 return count func _active_terrain_splat_count(focus_pos: Vector3) -> int: var count := _terrain_splat_cache_tasks.size() + _terrain_splat_tasks.size() for state_variant in _tile_states.values(): if not (state_variant is Dictionary): continue var state: Dictionary = state_variant if not bool(state.get("wanted", false)): continue var source := String(state.get("quality_terrain_source", "")) if source != "splat_cache" and source != "splat": continue if _tile_within_splat_terrain_radius(state, focus_pos): count += 1 return count func _prune_active_terrain_control_splat_tiles(focus_pos: Vector3) -> void: var limit := maxi(0, max_active_terrain_control_splat_tiles) if limit <= 0: for key_variant in _tile_states.keys(): var key := String(key_variant) var state: Dictionary = _tile_states[key] if String(state.get("quality_terrain_source", "")) == "control_splat_cache": state = _drop_splat_quality(state) state["desired_tile_lod"] = _compute_desired_tile_lod(state, focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos) _tile_states[key] = state return var active: Array = [] for key_variant in _tile_states.keys(): var key := String(key_variant) var state: Dictionary = _tile_states[key] if String(state.get("quality_terrain_source", "")) != "control_splat_cache": continue if not bool(state.get("wanted", false)) or not _tile_within_control_splat_terrain_radius(state, focus_pos): state = _drop_splat_quality(state) state["desired_tile_lod"] = _compute_desired_tile_lod(state, focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos) _tile_states[key] = state continue active.append({ "key": key, "dist_sq": _tile_dist_sq(state, focus_pos), "focus": _tile_is_focus_tile(state, focus_pos), }) if active.size() <= limit: return active.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: if bool(a["focus"]) != bool(b["focus"]): return bool(a["focus"]) return float(a["dist_sq"]) < float(b["dist_sq"])) var keep := {} for i in range(mini(limit, active.size())): keep[String(active[i]["key"])] = true for row in active: var key := String(row["key"]) if keep.has(key) or not _tile_states.has(key): continue var state: Dictionary = _tile_states[key] state = _drop_splat_quality(state) state["desired_tile_lod"] = _compute_desired_tile_lod(state, focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos) _tile_states[key] = state func _prune_active_terrain_splat_tiles(focus_pos: Vector3) -> void: var limit := maxi(0, max_active_terrain_splat_tiles) if limit <= 0: for key_variant in _tile_states.keys(): var key := String(key_variant) var state: Dictionary = _tile_states[key] var source := String(state.get("quality_terrain_source", "")) if source == "splat_cache" or source == "splat": state = _drop_splat_quality(state) state["desired_tile_lod"] = _compute_desired_tile_lod(state, focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos) _tile_states[key] = state return var active: Array = [] for key_variant in _tile_states.keys(): var key := String(key_variant) var state: Dictionary = _tile_states[key] var source := String(state.get("quality_terrain_source", "")) if source != "splat_cache" and source != "splat": continue if not bool(state.get("wanted", false)) or not _tile_within_splat_terrain_radius(state, focus_pos): state = _drop_splat_quality(state) state["desired_tile_lod"] = _compute_desired_tile_lod(state, focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos) _tile_states[key] = state continue active.append({ "key": key, "dist_sq": _tile_dist_sq(state, focus_pos), "focus": _tile_is_focus_tile(state, focus_pos), }) if active.size() <= limit: return active.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: if bool(a["focus"]) != bool(b["focus"]): return bool(a["focus"]) return float(a["dist_sq"]) < float(b["dist_sq"])) var keep := {} for i in range(mini(limit, active.size())): keep[String(active[i]["key"])] = true for row in active: var key := String(row["key"]) if keep.has(key): continue if not _tile_states.has(key): continue var state: Dictionary = _tile_states[key] state = _drop_splat_quality(state) state["desired_tile_lod"] = _compute_desired_tile_lod(state, focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos) _tile_states[key] = state func _tile_is_focus_tile(state: Dictionary, focus_pos: Vector3) -> bool: var focus_tile := _world_to_tile(focus_pos) return int(state.get("tx", -9999)) == focus_tile.x and int(state.get("ty", -9999)) == focus_tile.y func _request_terrain_control_splat_cache(state: Dictionary) -> bool: var key := String(state.get("key", "")) if key.is_empty(): return false if _terrain_control_splat_cache_tasks.has(key): return true var cache_path := _get_terrain_control_splat_resource_path( int(state.get("tx", 0)), int(state.get("ty", 0))) if not ResourceLoader.exists(cache_path): return false var err := ResourceLoader.load_threaded_request( cache_path, "", false, ResourceLoader.CACHE_MODE_IGNORE) if err == OK or err == ERR_BUSY: _terrain_control_splat_cache_tasks[key] = { "path": cache_path, "tx": int(state.get("tx", 0)), "ty": int(state.get("ty", 0)), } return true return false func _load_terrain_control_splat_immediate(state: Dictionary, focus_pos: Vector3) -> Dictionary: var key := String(state.get("key", "")) if key.is_empty(): return state var cache_path := _get_terrain_control_splat_resource_path( int(state.get("tx", 0)), int(state.get("ty", 0))) if not ResourceLoader.exists(cache_path): return state var control_tile: Resource = ResourceLoader.load(cache_path, "", ResourceLoader.CACHE_MODE_IGNORE) if control_tile == null or control_tile.get_script() != CONTROL_SPLAT_TILE_SCRIPT: push_warning("Invalid immediate control splat tile resource: %s" % cache_path) return state if int(control_tile.get("format_version")) < REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION: push_warning("Outdated immediate control splat tile resource: %s" % cache_path) return state var mesh: Mesh = control_tile.get("terrain_mesh") if mesh == null: push_warning("Immediate control splat tile has no terrain mesh: %s" % cache_path) return state mesh = mesh.duplicate(true) if _builder.has_method("apply_control_splat_material"): mesh = _builder.apply_control_splat_material( mesh, control_tile.get("texture_images"), control_tile.get("alpha_atlas"), control_tile.get("layer_index_map")) if mesh == null or mesh.surface_get_material(0) == null: push_warning("Immediate control splat material failed: %s" % cache_path) return state state["quality_terrain_mesh"] = mesh state["quality_terrain_source"] = "control_splat_cache" state["desired_tile_lod"] = _compute_desired_tile_lod(state, focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, focus_pos) _terrain_control_splat_cache_tasks.erase(key) var cache_key := _tile_mesh_cache_key(key, 0) _tile_mesh_cache.erase(cache_key) _tile_mesh_cache_order.erase(cache_key) return state func _request_terrain_splat_cache(state: Dictionary) -> bool: var key := String(state.get("key", "")) if key.is_empty(): return false if _terrain_splat_cache_tasks.has(key): return true var cache_path := _get_terrain_splat_resource_path( int(state.get("tx", 0)), int(state.get("ty", 0))) if not ResourceLoader.exists(cache_path): return false var err := ResourceLoader.load_threaded_request( cache_path, "", false, ResourceLoader.CACHE_MODE_IGNORE) if err == OK or err == ERR_BUSY: _terrain_splat_cache_tasks[key] = { "path": cache_path, "tx": int(state.get("tx", 0)), "ty": int(state.get("ty", 0)), } return true return false func _drain_terrain_upgrade_results() -> void: var ready: Array[String] = [] var budget := maxi(0, terrain_upgrade_finalize_ops_per_tick) if budget <= 0: return for key_variant in _terrain_upgrade_tasks.keys(): if ready.size() >= budget: break var key := String(key_variant) var pending: Dictionary = _terrain_upgrade_tasks[key] var status := ResourceLoader.load_threaded_get_status(String(pending.get("path", ""))) if status == ResourceLoader.THREAD_LOAD_LOADED or status == ResourceLoader.THREAD_LOAD_FAILED: ready.append(key) var changed := false for key in ready: if not _terrain_upgrade_tasks.has(key): continue var pending: Dictionary = _terrain_upgrade_tasks[key] _terrain_upgrade_tasks.erase(key) var path := String(pending.get("path", "")) var status := ResourceLoader.load_threaded_get_status(path) if status != ResourceLoader.THREAD_LOAD_LOADED: continue var baked_tile: Resource = ResourceLoader.load_threaded_get(path) if baked_tile == null or baked_tile.get_script() != BAKED_TILE_SCRIPT: continue if not _tile_states.has(key): continue var state: Dictionary = _tile_states[key] if state.get("stream_tile", null) == null: continue if String(state.get("quality_terrain_source", "")) == "control_splat_cache": continue var baked_version: int = int(baked_tile.get("format_version")) if baked_version < REQUIRED_BAKED_TILE_FORMAT_VERSION: continue state["baked_tile"] = baked_tile var full_mesh: Mesh = baked_tile.get("full_mesh") if full_mesh != null: full_mesh = full_mesh.duplicate(true) if _builder.has_method("upgrade_cached_baked_mesh_materials"): full_mesh = _builder.upgrade_cached_baked_mesh_materials(full_mesh, 1.0, -1.25, 0.35) state["quality_terrain_mesh"] = full_mesh state["quality_terrain_source"] = "baked_full" _store_quality_terrain_mesh(key, full_mesh, "baked_full") state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos) _tile_states[key] = state _tile_mesh_cache.erase(_tile_mesh_cache_key(key, 0)) _tile_mesh_cache_order.erase(_tile_mesh_cache_key(key, 0)) changed = true if changed: _rebuild_chunk_queues() func _drain_terrain_control_splat_cache_results() -> void: var ready: Array[String] = [] var budget := maxi(0, terrain_control_splat_cache_finalize_ops_per_tick) if budget <= 0: return for key_variant in _terrain_control_splat_cache_tasks.keys(): if ready.size() >= budget: break var key := String(key_variant) var pending: Dictionary = _terrain_control_splat_cache_tasks[key] var status := ResourceLoader.load_threaded_get_status(String(pending.get("path", ""))) if status == ResourceLoader.THREAD_LOAD_LOADED or status == ResourceLoader.THREAD_LOAD_FAILED: ready.append(key) var changed := false for key in ready: if not _terrain_control_splat_cache_tasks.has(key): continue var pending: Dictionary = _terrain_control_splat_cache_tasks[key] _terrain_control_splat_cache_tasks.erase(key) var path := String(pending.get("path", "")) var status := ResourceLoader.load_threaded_get_status(path) if status != ResourceLoader.THREAD_LOAD_LOADED: continue var control_tile: Resource = ResourceLoader.load_threaded_get(path) if control_tile == null or control_tile.get_script() != CONTROL_SPLAT_TILE_SCRIPT: continue if int(control_tile.get("format_version")) < REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION: continue if not _tile_states.has(key): continue var state: Dictionary = _tile_states[key] if not bool(state.get("wanted", false)): continue if not _tile_within_control_splat_terrain_radius(state, _last_focus_pos): continue if not _can_activate_terrain_control_splat(key, state, _last_focus_pos): continue var mesh: Mesh = control_tile.get("terrain_mesh") if mesh == null: continue mesh = mesh.duplicate(true) if _builder.has_method("apply_control_splat_material"): mesh = _builder.apply_control_splat_material( mesh, control_tile.get("texture_images"), control_tile.get("alpha_atlas"), control_tile.get("layer_index_map")) state["quality_terrain_mesh"] = mesh state["quality_terrain_source"] = "control_splat_cache" state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos) _tile_states[key] = state var cache_key := _tile_mesh_cache_key(key, 0) _tile_mesh_cache.erase(cache_key) _tile_mesh_cache_order.erase(cache_key) changed = true if changed: _rebuild_chunk_queues() func _drain_terrain_splat_cache_results() -> void: var ready: Array[String] = [] var budget := maxi(0, terrain_splat_cache_finalize_ops_per_tick) if budget <= 0: return for key_variant in _terrain_splat_cache_tasks.keys(): if ready.size() >= budget: break var key := String(key_variant) var pending: Dictionary = _terrain_splat_cache_tasks[key] var status := ResourceLoader.load_threaded_get_status(String(pending.get("path", ""))) if status == ResourceLoader.THREAD_LOAD_LOADED or status == ResourceLoader.THREAD_LOAD_FAILED: ready.append(key) var changed := false for key in ready: if not _terrain_splat_cache_tasks.has(key): continue var pending: Dictionary = _terrain_splat_cache_tasks[key] _terrain_splat_cache_tasks.erase(key) var path := String(pending.get("path", "")) var status := ResourceLoader.load_threaded_get_status(path) if status != ResourceLoader.THREAD_LOAD_LOADED: continue var splat_tile: Resource = ResourceLoader.load_threaded_get(path) if splat_tile == null or splat_tile.get_script() != SPLAT_TILE_SCRIPT: continue if int(splat_tile.get("format_version")) < REQUIRED_SPLAT_TILE_FORMAT_VERSION: continue if not _tile_states.has(key): continue var state: Dictionary = _tile_states[key] if not bool(state.get("wanted", false)): continue if not _tile_within_splat_terrain_radius(state, _last_focus_pos): continue if not _can_activate_terrain_splat(key, state, _last_focus_pos): continue var mesh: Mesh = splat_tile.get("terrain_mesh") if mesh == null: continue state["quality_terrain_mesh"] = mesh state["quality_terrain_source"] = "splat_cache" state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos) _tile_states[key] = state var cache_key := _tile_mesh_cache_key(key, 0) _tile_mesh_cache.erase(cache_key) _tile_mesh_cache_order.erase(cache_key) changed = true if changed: _rebuild_chunk_queues() func _build_high_quality_stream_terrain_mesh(stream_mesh: Mesh, full_mesh: Mesh) -> Mesh: if stream_mesh == null or full_mesh == null: return null var upgraded := stream_mesh.duplicate(true) as ArrayMesh if upgraded == null: return null var surface_count := mini(upgraded.get_surface_count(), full_mesh.get_surface_count()) for surface_idx in surface_count: var material := full_mesh.surface_get_material(surface_idx) if material != null: upgraded.surface_set_material(surface_idx, material) return upgraded func _load_terrain_splat_task(key: String, adt_path: String) -> void: var data: Dictionary = {} if not adt_path.is_empty(): var loader = ClassDB.instantiate("ADTLoader") if loader: data = loader.call("load_adt", adt_path) _terrain_splat_result_mutex.lock() _terrain_splat_result_queue.append({ "key": key, "data": data, }) _terrain_splat_result_mutex.unlock() func _drain_terrain_splat_results() -> void: var results: Array = [] var budget := maxi(0, terrain_splat_builds_per_tick) if budget <= 0: return _terrain_splat_result_mutex.lock() while budget > 0 and not _terrain_splat_result_queue.is_empty(): results.append(_terrain_splat_result_queue.pop_front()) budget -= 1 _terrain_splat_result_mutex.unlock() var changed := false for result_variant in results: if not (result_variant is Dictionary): continue var result: Dictionary = result_variant var key := String(result.get("key", "")) if _terrain_splat_tasks.has(key): WorkerThreadPool.wait_for_task_completion(int(_terrain_splat_tasks[key])) _terrain_splat_tasks.erase(key) if not _tile_states.has(key): continue var state: Dictionary = _tile_states[key] if not bool(state.get("wanted", false)): continue if not _tile_within_splat_terrain_radius(state, _last_focus_pos): continue if not _can_activate_terrain_splat(key, state, _last_focus_pos): continue var data: Dictionary = result.get("data", {}) if data.is_empty(): continue var payload: Dictionary = _builder.build_tile_coarse_render_payload( data, _shared_tex_cache, ProjectSettings.globalize_path(extracted_dir), maxi(0, terrain_splat_lod)) var mesh: Mesh = payload.get("mesh", null) if mesh == null: continue state["quality_terrain_mesh"] = mesh state["quality_terrain_source"] = "splat" state["data"] = data state["tex_names"] = data.get("textures", PackedStringArray()) state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos) _tile_states[key] = state var cache_key := _tile_mesh_cache_key(key, 0) _tile_mesh_cache.erase(cache_key) _tile_mesh_cache_order.erase(cache_key) changed = true if changed: _rebuild_chunk_queues() func _request_tile_water_load(state: Dictionary) -> void: if not enable_water: return if bool(state.get("water_loaded", false)): return var key := String(state.get("key", "")) var path := String(state.get("path", "")) if key.is_empty() or path.is_empty(): return if _water_load_tasks.has(key) or _water_load_queued.has(key): return _water_load_queue.append({ "key": key, "path": path, }) _water_load_queued[key] = true func _process_water_load_queue() -> void: if not enable_water: return var limit := maxi(1, max_concurrent_water_tasks) while _water_load_tasks.size() < limit and not _water_load_queue.is_empty(): var request: Dictionary = _water_load_queue.pop_front() var key := String(request.get("key", "")) _water_load_queued.erase(key) if key.is_empty() or _water_load_tasks.has(key): continue if not _tile_states.has(key): continue var path := String(request.get("path", "")) if path.is_empty(): continue var task_id: int = WorkerThreadPool.add_task(_load_tile_water_task.bind(key, path)) _water_load_tasks[key] = task_id func _load_tile_water_task(key: String, adt_path: String) -> void: var data: Dictionary = {} var loader = ClassDB.instantiate("ADTLoader") if loader: data = loader.call("load_adt", adt_path) _water_result_mutex.lock() _water_result_queue.append({ "key": key, "path": adt_path, "data": data, }) _water_result_mutex.unlock() func _drain_water_load_results() -> void: var results: Array = [] var budget := maxi(0, water_finalize_ops_per_tick) if budget <= 0: return _water_result_mutex.lock() while budget > 0 and not _water_result_queue.is_empty(): results.append(_water_result_queue.pop_front()) budget -= 1 _water_result_mutex.unlock() for result in results: var key := String(result.get("key", "")) if key.is_empty(): continue if not _water_load_tasks.has(key): continue WorkerThreadPool.wait_for_task_completion(int(_water_load_tasks[key])) _water_load_tasks.erase(key) if _shutting_down or not _tile_states.has(key): continue var state: Dictionary = _tile_states[key] if String(state.get("path", "")) != String(result.get("path", "")): continue var tile_root: Node = state.get("root", null) if tile_root == null or not is_instance_valid(tile_root): continue var data: Dictionary = result.get("data", {}) if not data.is_empty(): var origin: Vector3 = state.get("origin", Vector3.ZERO) var water_root: Node3D = _builder.build_tile_water_scene(data, origin) if water_root: tile_root.add_child(water_root) _set_editor_owner_recursive(water_root) state["water_loaded"] = true _tile_states[key] = state func _wait_for_tile_tasks() -> void: for pending in _tile_loading_tasks.values(): if pending.get("mode", "raw") == "raw": WorkerThreadPool.wait_for_task_completion(int(pending["task_id"])) elif pending.get("mode", "raw") == "baked" or pending.get("mode", "raw") == "stream": var path: String = String(pending.get("path", "")) if not path.is_empty(): var status := ResourceLoader.load_threaded_get_status(path) if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED: ResourceLoader.load_threaded_get(path) _tile_loading_tasks.clear() for task_id in _m2_group_tasks.values(): WorkerThreadPool.wait_for_task_completion(int(task_id)) _m2_group_tasks.clear() _tile_result_mutex.lock() _tile_result_queue.clear() _tile_result_mutex.unlock() _m2_group_result_mutex.lock() _m2_group_result_queue.clear() _m2_group_result_mutex.unlock() for pending in _m2_mesh_load_requests.values(): var path: String = String(pending.get("path", "")) if path.is_empty(): continue var status := ResourceLoader.load_threaded_get_status(path) if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED: ResourceLoader.load_threaded_get(path) _m2_mesh_load_requests.clear() _m2_mesh_finalize_queue.clear() _m2_runtime_rebuild_required_cache.clear() for pending in _m2_animation_load_requests.values(): var path: String = String(pending.get("path", "")) if path.is_empty(): continue var status := ResourceLoader.load_threaded_get_status(path) if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED: ResourceLoader.load_threaded_get(path) _m2_animation_load_requests.clear() _m2_animation_finalize_queue.clear() for path_variant in _wmo_scene_load_requests.values(): var path := String(path_variant) if path.is_empty(): continue var status := ResourceLoader.load_threaded_get_status(path) if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED: ResourceLoader.load_threaded_get(path) _wmo_scene_load_requests.clear() for path_variant in _wmo_render_load_requests.values(): var path := String(path_variant) if path.is_empty(): continue var status := ResourceLoader.load_threaded_get_status(path) if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED: ResourceLoader.load_threaded_get(path) _wmo_render_load_requests.clear() _wmo_render_missing_cache.clear() _wmo_scene_cache_missing.clear() _wmo_render_build_jobs.clear() _wmo_render_build_queue.clear() for pending in _terrain_upgrade_tasks.values(): var path := String(pending.get("path", "")) if path.is_empty(): continue var status := ResourceLoader.load_threaded_get_status(path) if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED: ResourceLoader.load_threaded_get(path) _terrain_upgrade_tasks.clear() for pending in _terrain_control_splat_cache_tasks.values(): if not (pending is Dictionary): continue var path := String(pending.get("path", "")) if path.is_empty(): continue var status := ResourceLoader.load_threaded_get_status(path) if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED: ResourceLoader.load_threaded_get(path) _terrain_control_splat_cache_tasks.clear() for task_id in _terrain_splat_tasks.values(): WorkerThreadPool.wait_for_task_completion(int(task_id)) _terrain_splat_tasks.clear() for pending in _terrain_splat_cache_tasks.values(): if not (pending is Dictionary): continue var path := String(pending.get("path", "")) if path.is_empty(): continue var status := ResourceLoader.load_threaded_get_status(path) if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED: ResourceLoader.load_threaded_get(path) _terrain_splat_cache_tasks.clear() _terrain_splat_result_mutex.lock() _terrain_splat_result_queue.clear() _terrain_splat_result_mutex.unlock() for task_id in _water_load_tasks.values(): WorkerThreadPool.wait_for_task_completion(int(task_id)) _water_load_tasks.clear() _water_load_queue.clear() _water_load_queued.clear() _water_result_mutex.lock() _water_result_queue.clear() _water_result_mutex.unlock() func _should_accept_loaded_tile(request: Dictionary) -> bool: if _tile_states.has(request["key"]): return false if not _available_tiles.has(request["key"]): return false return _is_tile_wanted_now(int(request["tx"]), int(request["ty"])) func _is_tile_wanted_now(tx: int, ty: int) -> bool: var focus_tile := _world_to_tile(_last_focus_pos) var radius: int if Engine.is_editor_hint(): radius = maxi(0, editor_preview_tile_radius) else: radius = _runtime_load_tile_radius() return abs(tx - focus_tile.x) <= radius and abs(ty - focus_tile.y) <= radius func _finalize_loaded_tile(request: Dictionary, data: Dictionary) -> void: var key: String = request["key"] if _tile_states.has(key): return if data.is_empty() or not data.has("chunks"): push_warning("Failed to load ADT tile: %s" % request["path"]) return var tile_root := Node3D.new() tile_root.name = "Tile_%d_%d" % [request["tx"], request["ty"]] _terrain_root.add_child(tile_root) _set_editor_owner_recursive(tile_root) var tile_origin: Vector3 = _builder.get_tile_origin_for_data(data) tile_root.position = tile_origin if enable_water: var water_root: Node3D = _builder.build_tile_water_scene(data, tile_origin) if water_root: tile_root.add_child(water_root) _set_editor_owner_recursive(water_root) if enable_m2_placeholders: _build_tile_m2_placeholders( tile_root, tile_origin, data.get("m2_names", PackedStringArray()), data.get("m2_placements", [])) var state := { "key": key, "tx": request["tx"], "ty": request["ty"], "path": request["path"], "origin": tile_origin, "data": data, "baked_tile": null, "tex_names": data.get("textures", PackedStringArray()), "chunks": {}, "desired_lods": {}, "tile_lod": -1, "tile_lod_node": null, "tile_lod_rid": RID(), "tile_lod_mesh": null, "tile_lod_visible": false, "prepared_tile_lod": -1, "prepared_tile_lod_rid": RID(), "prepared_tile_lod_mesh": null, "desired_tile_lod": -1, "desired_tile_lod_visible": false, "wanted": true, "retained": true, "root": tile_root, "water_loaded": true, "m2_names": data.get("m2_names", PackedStringArray()), "m2_placements": data.get("m2_placements", []), "m2_built": false, "m2_unique_keys": [], "m2_skipped_unique_keys": [], "wmo_names": data.get("wmo_names", PackedStringArray()), "wmo_placements": data.get("wmo_placements", []), "wmo_refs": [], } # Use _last_focus_pos — already set correctly for both editor and game. state["desired_lods"] = _compute_desired_lods(state, _last_focus_pos) state["desired_tile_lod"] = -1 if not state["desired_lods"].is_empty() else _compute_desired_tile_lod(state, _last_focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos) _tile_states[key] = state state = _sync_terrain_quality(state, _last_focus_pos) state = _sync_detail_assets(state, _last_focus_pos) _tile_states[key] = state _rebuild_chunk_queues() if debug_streaming: print("StreamingWorld: loaded tile %d,%d" % [request["tx"], request["ty"]]) func _finalize_loaded_baked_tile(request: Dictionary, baked_tile: Resource) -> void: var key: String = request["key"] if _tile_states.has(key): return var baked_version: int = int(baked_tile.get("format_version")) if baked_version < REQUIRED_BAKED_TILE_FORMAT_VERSION: push_warning( "Outdated baked tile cache for %s_%d_%d (format=%d, required=%d). Falling back to raw ADT load." % [ map_name, request["tx"], request["ty"], baked_version, REQUIRED_BAKED_TILE_FORMAT_VERSION ] ) _start_raw_tile_load_async(request) return var tile_root := Node3D.new() tile_root.name = "Tile_%d_%d" % [request["tx"], request["ty"]] _terrain_root.add_child(tile_root) _set_editor_owner_recursive(tile_root) var tile_origin: Vector3 = baked_tile.get("tile_origin") tile_root.position = tile_origin var baked_m2_names_value: Variant = baked_tile.get("m2_names") var baked_m2_placements_value: Variant = baked_tile.get("m2_placements") var baked_m2_names: PackedStringArray = baked_m2_names_value if baked_m2_names_value is PackedStringArray else PackedStringArray() var baked_m2_placements: Array = baked_m2_placements_value if baked_m2_placements_value is Array else [] var baked_wmo_names_value: Variant = baked_tile.get("wmo_names") var baked_wmo_placements_value: Variant = baked_tile.get("wmo_placements") var baked_wmo_names: PackedStringArray = baked_wmo_names_value if baked_wmo_names_value is PackedStringArray else PackedStringArray() var baked_wmo_placements: Array = baked_wmo_placements_value if baked_wmo_placements_value is Array else [] var state := { "key": key, "tx": request["tx"], "ty": request["ty"], "path": request["path"], "origin": tile_origin, "data": {}, "baked_tile": baked_tile, "tex_names": PackedStringArray(), "chunks": {}, "desired_lods": {}, "tile_lod": -1, "tile_lod_node": null, "tile_lod_rid": RID(), "tile_lod_mesh": null, "tile_lod_visible": false, "prepared_tile_lod": -1, "prepared_tile_lod_rid": RID(), "prepared_tile_lod_mesh": null, "desired_tile_lod": -1, "desired_tile_lod_visible": false, "wanted": true, "retained": true, "root": tile_root, "water_loaded": false, "m2_names": baked_m2_names, "m2_placements": baked_m2_placements, "m2_built": false, "m2_unique_keys": [], "m2_skipped_unique_keys": [], "wmo_names": baked_wmo_names, "wmo_placements": baked_wmo_placements, "wmo_refs": [], } state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos) _tile_states[key] = state _request_tile_water_load(state) state = _sync_detail_assets(state, _last_focus_pos) _tile_states[key] = state _rebuild_chunk_queues() if debug_streaming: print("StreamingWorld: loaded baked tile %d,%d" % [request["tx"], request["ty"]]) func _finalize_loaded_streaming_tile(request: Dictionary, stream_tile: Resource) -> void: var key: String = request["key"] if _tile_states.has(key): return var stream_version: int = int(stream_tile.get("format_version")) if stream_version < STREAMING_TILE_SCRIPT.FORMAT_VERSION: push_warning( "Outdated streaming tile cache for %s_%d_%d (format=%d, required=%d). Falling back to baked/raw ADT load." % [ map_name, request["tx"], request["ty"], stream_version, STREAMING_TILE_SCRIPT.FORMAT_VERSION ] ) _start_raw_tile_load_async(request) return var tile_root := Node3D.new() tile_root.name = "Tile_%d_%d" % [request["tx"], request["ty"]] _terrain_root.add_child(tile_root) _set_editor_owner_recursive(tile_root) var tile_origin: Vector3 = stream_tile.get("tile_origin") tile_root.position = tile_origin var stream_m2_names_value: Variant = stream_tile.get("m2_names") var stream_m2_placements_value: Variant = stream_tile.get("m2_placements") var stream_m2_names: PackedStringArray = stream_m2_names_value if stream_m2_names_value is PackedStringArray else PackedStringArray() var stream_m2_placements: Array = stream_m2_placements_value if stream_m2_placements_value is Array else [] var stream_wmo_names_value: Variant = stream_tile.get("wmo_names") var stream_wmo_placements_value: Variant = stream_tile.get("wmo_placements") var stream_wmo_names: PackedStringArray = stream_wmo_names_value if stream_wmo_names_value is PackedStringArray else PackedStringArray() var stream_wmo_placements: Array = stream_wmo_placements_value if stream_wmo_placements_value is Array else [] var state := { "key": key, "tx": request["tx"], "ty": request["ty"], "path": request["path"], "origin": tile_origin, "data": {}, "baked_tile": null, "stream_tile": stream_tile, "tex_names": PackedStringArray(), "chunks": {}, "desired_lods": {}, "tile_lod": -1, "tile_lod_node": null, "tile_lod_rid": RID(), "tile_lod_mesh": null, "tile_lod_visible": false, "prepared_tile_lod": -1, "prepared_tile_lod_rid": RID(), "prepared_tile_lod_mesh": null, "desired_tile_lod": -1, "desired_tile_lod_visible": false, "wanted": true, "retained": true, "root": tile_root, "water_loaded": false, "m2_names": stream_m2_names, "m2_placements": stream_m2_placements, "m2_built": false, "m2_unique_keys": [], "m2_skipped_unique_keys": [], "wmo_names": stream_wmo_names, "wmo_placements": stream_wmo_placements, "wmo_refs": [], } state = _restore_quality_terrain_mesh(key, state) state["desired_tile_lod"] = _compute_desired_tile_lod(state, _last_focus_pos) state["desired_tile_lod_visible"] = _compute_desired_tile_lod_visible(state, _last_focus_pos) _tile_states[key] = state _request_tile_water_load(state) state = _sync_terrain_quality(state, _last_focus_pos) state = _sync_detail_assets(state, _last_focus_pos) _tile_states[key] = state _rebuild_chunk_queues() if debug_streaming: print("StreamingWorld: loaded streaming tile %d,%d" % [request["tx"], request["ty"]]) func _create_tile_lod(op: Dictionary) -> void: var key: String = op["tile"] if not _tile_states.has(key): return var state: Dictionary = _tile_states[key] var lod: int = int(op["lod"]) var visible: bool = bool(op.get("visible", true)) var prepare_only: bool = bool(op.get("prepare", false)) if int(state.get("tile_lod", -1)) == lod: _set_tile_lod_visible(state, visible) state["tile_lod_visible"] = visible _tile_states[key] = state return if prepare_only: if int(state.get("prepared_tile_lod", -1)) == lod: return var prepared_mesh := _get_tile_lod_mesh_for_state(key, state, lod) if prepared_mesh == null or not use_rendering_server_for_terrain: return var prepared_rid := _create_render_instance( prepared_mesh, _tile_local_to_world_transform(state, Vector3.ZERO)) if not prepared_rid.is_valid(): return RenderingServer.instance_set_visible(prepared_rid, false) var old_prepared_rid: RID = state.get("prepared_tile_lod_rid", RID()) if old_prepared_rid.is_valid(): _free_render_instance(old_prepared_rid) state["prepared_tile_lod"] = lod state["prepared_tile_lod_rid"] = prepared_rid state["prepared_tile_lod_mesh"] = prepared_mesh _tile_states[key] = state return var old_node: Node = state.get("tile_lod_node", null) var old_rid: RID = state.get("tile_lod_rid", RID()) var prepared_lod: int = int(state.get("prepared_tile_lod", -1)) var prepared_rid: RID = state.get("prepared_tile_lod_rid", RID()) if prepared_lod == lod and prepared_rid.is_valid(): state["tile_lod"] = lod state["tile_lod_node"] = null state["tile_lod_rid"] = prepared_rid state["tile_lod_mesh"] = state.get("prepared_tile_lod_mesh", null) state["prepared_tile_lod"] = -1 state["prepared_tile_lod_rid"] = RID() state["prepared_tile_lod_mesh"] = null RenderingServer.instance_set_visible(prepared_rid, visible) if old_rid.is_valid(): _free_render_instance(old_rid) if old_node: old_node.queue_free() state["tile_lod_visible"] = visible _tile_states[key] = state return state["tile_lod"] = lod state["tile_lod_node"] = null state["tile_lod_rid"] = RID() state["tile_lod_mesh"] = null var mesh := _get_tile_lod_mesh_for_state(key, state, lod) if use_rendering_server_for_terrain and mesh: var instance_rid := _create_render_instance( mesh, _tile_local_to_world_transform(state, Vector3.ZERO)) if instance_rid.is_valid(): RenderingServer.instance_set_visible(instance_rid, visible) state["tile_lod_rid"] = instance_rid state["tile_lod_mesh"] = mesh if not state["tile_lod_rid"].is_valid() and mesh: var node := MeshInstance3D.new() node.name = "TileLOD%d" % lod node.mesh = mesh node.visible = visible node.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON if terrain_cast_shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF state["tile_lod_node"] = node state["tile_lod_mesh"] = mesh state["root"].add_child(node) _set_editor_owner_recursive(node) if old_rid.is_valid(): _free_render_instance(old_rid) if old_node: old_node.queue_free() state["tile_lod_visible"] = visible _tile_states[key] = state func _get_tile_lod_mesh_for_state(key: String, state: Dictionary, lod: int) -> Mesh: if lod <= 0: var quality_mesh: Mesh = state.get("quality_terrain_mesh", null) if quality_mesh != null: return quality_mesh var cache_key := _tile_mesh_cache_key(key, lod) var mesh: Mesh = _get_cached_tile_mesh(cache_key) if mesh != null: return mesh mesh = _get_baked_tile_mesh(state, lod) if mesh == null: var payload: Dictionary = _builder.build_tile_coarse_render_payload( state["data"], _shared_tex_cache, ProjectSettings.globalize_path(extracted_dir), lod) if not payload.is_empty(): mesh = payload.get("mesh", null) if mesh: _store_cached_tile_mesh(cache_key, mesh) return mesh func _create_chunk(op: Dictionary) -> void: var key: String = op["tile"] if not _tile_states.has(key): return var state: Dictionary = _tile_states[key] var chunk_id: int = int(op["chunk"]) var lod: int = int(op["lod"]) var chunks: Array = state["data"].get("chunks", []) if chunk_id < 0 or chunk_id >= chunks.size(): return var chunk: Dictionary = chunks[chunk_id] if chunk.is_empty(): return var current_chunks: Dictionary = state["chunks"] if current_chunks.has(chunk_id): _remove_chunk({"tile": key, "chunk": chunk_id}) current_chunks = state["chunks"] var instance_rid := RID() var node: MeshInstance3D = null var mesh: Mesh = null if use_rendering_server_for_terrain: var payload: Dictionary = _builder.build_chunk_render_payload( chunk, state["tex_names"], _shared_tex_cache, ProjectSettings.globalize_path(extracted_dir), lod, state.get("origin", Vector3.ZERO)) if not payload.is_empty(): mesh = payload.get("mesh", null) instance_rid = _create_render_instance( mesh, _tile_local_to_world_transform(state, payload.get("position", Vector3.ZERO))) if not instance_rid.is_valid(): node = _builder.build_chunk_scene( chunk, state["tex_names"], _shared_tex_cache, ProjectSettings.globalize_path(extracted_dir), lod, state.get("origin", Vector3.ZERO)) if node == null and not instance_rid.is_valid(): _dbg_chunks_null += 1 current_chunks[chunk_id] = { "node": null, "rid": RID(), "mesh": null, "lod": lod, "empty": true, } state["chunks"] = current_chunks _tile_states[key] = state if debug_streaming and _dbg_chunks_null <= 3: var h: PackedFloat32Array = chunk.get("heights", PackedFloat32Array()) print("DBG NULL chunk tile=%s id=%d lod=%d heights=%d origin=%s" % [ key, chunk_id, lod, h.size(), chunk.get("origin", Vector3.ZERO)]) return if node: node.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_ON if terrain_cast_shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF state["root"].add_child(node) _set_editor_owner_recursive(node) _dbg_chunks_created += 1 if debug_streaming and _dbg_chunks_created <= 3: var dbg_origin := node.global_position if node else _tile_local_to_world_transform( state, chunk.get("origin", Vector3.ZERO) - state.get("origin", Vector3.ZERO)).origin print("DBG CHUNK ADDED tile=%s id=%d lod=%d world_pos=%s rs=%s" % [ key, chunk_id, lod, dbg_origin, instance_rid.is_valid()]) current_chunks[chunk_id] = { "node": node, "rid": instance_rid, "mesh": mesh, "lod": lod, } state["chunks"] = current_chunks _tile_states[key] = state func _remove_chunk(op: Dictionary) -> void: var key: String = op["tile"] if not _tile_states.has(key): return var state: Dictionary = _tile_states[key] var chunk_id: int = int(op["chunk"]) var current_chunks: Dictionary = state["chunks"] if not current_chunks.has(chunk_id): return var entry: Dictionary = current_chunks[chunk_id] var node: Node = entry.get("node", null) var instance_rid: RID = entry.get("rid", RID()) if instance_rid.is_valid(): _free_render_instance(instance_rid) if node: node.queue_free() current_chunks.erase(chunk_id) state["chunks"] = current_chunks if not bool(state.get("retained", false)) and current_chunks.is_empty(): _release_tile_if_unused(key) else: _tile_states[key] = state func _remove_tile_lod(op: Dictionary) -> void: var key: String = op["tile"] if not _tile_states.has(key): return var state: Dictionary = _tile_states[key] var node: Node = state.get("tile_lod_node", null) var instance_rid: RID = state.get("tile_lod_rid", RID()) if instance_rid.is_valid(): _free_render_instance(instance_rid) if node: node.queue_free() state["tile_lod_node"] = null state["tile_lod_rid"] = RID() state["tile_lod_mesh"] = null state["tile_lod_visible"] = false state["tile_lod"] = -1 if not bool(state.get("retained", false)) and state["chunks"].is_empty(): _release_tile_if_unused(key) else: _tile_states[key] = state func _set_tile_lod_visible(state: Dictionary, visible: bool) -> void: var instance_rid: RID = state.get("tile_lod_rid", RID()) if instance_rid.is_valid(): RenderingServer.instance_set_visible(instance_rid, visible) var node: Node = state.get("tile_lod_node", null) if node and is_instance_valid(node): node.visible = visible func _release_tile(key: String) -> void: if not _tile_states.has(key): return var state: Dictionary = _tile_states[key] _unregister_tile_wmos(state) _remove_tile_m2_assets(state) _terrain_upgrade_tasks.erase(key) _terrain_control_splat_cache_tasks.erase(key) _terrain_splat_cache_tasks.erase(key) _terrain_splat_tasks.erase(key) _water_load_queued.erase(key) for i in range(_water_load_queue.size() - 1, -1, -1): var pending: Dictionary = _water_load_queue[i] if String(pending.get("key", "")) == key: _water_load_queue.remove_at(i) var tile_lod_rid: RID = state.get("tile_lod_rid", RID()) if tile_lod_rid.is_valid(): _free_render_instance(tile_lod_rid) var prepared_tile_lod_rid: RID = state.get("prepared_tile_lod_rid", RID()) if prepared_tile_lod_rid.is_valid(): _free_render_instance(prepared_tile_lod_rid) for entry in state.get("chunks", {}).values(): var chunk_rid: RID = entry.get("rid", RID()) if chunk_rid.is_valid(): _free_render_instance(chunk_rid) var tile_root: Node = state.get("root", null) if tile_root: tile_root.queue_free() _remove_tile_mesh_cache_entries(key) _tile_states.erase(key) func _clear_streamed_world() -> void: _tile_load_queue.clear() _chunk_remove_queue.clear() _chunk_create_queue.clear() _tile_lod_remove_queue.clear() _tile_lod_create_queue.clear() _terrain_upgrade_tasks.clear() _terrain_control_splat_cache_tasks.clear() _terrain_splat_cache_tasks.clear() _terrain_splat_tasks.clear() _terrain_splat_result_mutex.lock() _terrain_splat_result_queue.clear() _terrain_splat_result_mutex.unlock() _water_load_queue.clear() _water_load_queued.clear() _water_load_tasks.clear() _water_result_mutex.lock() _water_result_queue.clear() _water_result_mutex.unlock() _detail_asset_queue.clear() _detail_asset_queued.clear() for key in _m2_build_jobs.keys(): _cancel_m2_build_job(String(key)) _m2_build_queue.clear() _m2_unique_registry.clear() _m2_mesh_load_requests.clear() _m2_mesh_finalize_queue.clear() _m2_runtime_rebuild_required_cache.clear() _m2_animation_load_requests.clear() _m2_animation_finalize_queue.clear() _wmo_build_jobs.clear() _wmo_build_queue.clear() _wmo_scene_load_requests.clear() _wmo_render_load_requests.clear() _wmo_render_missing_cache.clear() _wmo_scene_cache_missing.clear() _wmo_render_build_jobs.clear() _wmo_render_build_queue.clear() _tile_result_mutex.lock() _tile_result_queue.clear() _tile_result_mutex.unlock() for key in _tile_states.keys(): var state: Dictionary = _tile_states[key] var tile_lod_rid: RID = state.get("tile_lod_rid", RID()) if tile_lod_rid.is_valid(): _free_render_instance(tile_lod_rid) var prepared_tile_lod_rid: RID = state.get("prepared_tile_lod_rid", RID()) if prepared_tile_lod_rid.is_valid(): _free_render_instance(prepared_tile_lod_rid) for entry in state.get("chunks", {}).values(): var chunk_rid: RID = entry.get("rid", RID()) if chunk_rid.is_valid(): _free_render_instance(chunk_rid) var tile_root: Node = state.get("root", null) if tile_root: _queue_free_streamed_node(tile_root) _tile_states.clear() _clear_tile_mesh_cache() _clear_quality_terrain_mesh_cache() _clear_wmo_registry() if not _shutting_down: _ensure_world_wmo_root() if enable_wmo: _place_global_wmo() ## During world shutdown, parented nodes are recursively owned by the world ## root. Queueing them separately from `_exit_tree()` can detach them from that ## deletion and leave empty Node objects alive at engine teardown. func _queue_free_streamed_node(node: Node) -> void: if node == null or not is_instance_valid(node): return if _shutting_down and node.is_inside_tree(): return node.queue_free() func _reset_terrain_root_children() -> void: if _terrain_root == null: return for child in _terrain_root.get_children(): _terrain_root.remove_child(child) child.free() func _compute_desired_lods(state: Dictionary, cam_pos: Vector3) -> Dictionary: if not use_chunk_terrain_lods: return {} var desired := {} var chunks: Array = state["data"].get("chunks", []) var lod0_sq: float = (lod0_radius_chunks * CHUNK_SIZE) ** 2 var lod1_sq: float = (lod1_radius_chunks * CHUNK_SIZE) ** 2 var lod2_sq: float = (lod2_radius_chunks * CHUNK_SIZE) ** 2 for chunk_id in range(chunks.size()): var chunk: Dictionary = chunks[chunk_id] if chunk.is_empty(): continue var origin: Vector3 = chunk.get("origin", Vector3.ZERO) var center := origin + Vector3(CHUNK_SIZE * 0.5, 0.0, CHUNK_SIZE * 0.5) var dx := center.x - cam_pos.x var dz := center.z - cam_pos.z var dist_sq := dx * dx + dz * dz if dist_sq <= lod0_sq: desired[chunk_id] = 0 elif dist_sq <= lod1_sq: desired[chunk_id] = 1 elif dist_sq <= lod2_sq: desired[chunk_id] = 2 return desired func _runtime_visible_tile_radius() -> int: var chunk_tile_radius: int = int(ceil(float(lod2_radius_chunks) / 16.0)) + warm_tile_margin return maxi(chunk_tile_radius, lod2_tile_radius) func _runtime_load_tile_radius() -> int: return _runtime_visible_tile_radius() + maxi(0, prewarm_tile_margin) func _runtime_retain_tile_radius() -> int: return _runtime_load_tile_radius() + maxi(0, retain_tile_margin) func _predictive_focus_tiles(focus_pos: Vector3) -> Array[Vector2i]: var base := _world_to_tile(focus_pos) var result: Array[Vector2i] = [base] var threshold: float = clampf(boundary_prefetch_threshold, 0.0, 0.49) if threshold <= 0.0: return result var local_x := fposmod(focus_pos.x, TILE_SIZE) / TILE_SIZE var local_z := fposmod(focus_pos.z, TILE_SIZE) / TILE_SIZE var x_offsets: Array[int] = [0] var z_offsets: Array[int] = [0] if local_x <= threshold: x_offsets.append(-1) elif local_x >= 1.0 - threshold: x_offsets.append(1) if local_z <= threshold: z_offsets.append(-1) elif local_z >= 1.0 - threshold: z_offsets.append(1) for ox in x_offsets: for oz in z_offsets: if ox == 0 and oz == 0: continue result.append(Vector2i(base.x + ox, base.y + oz)) return result func _compute_desired_tile_lod(state: Dictionary, cam_pos: Vector3) -> int: var tile_dist_sq: float = _tile_dist_sq(state, cam_pos) var chunk_radius_sq: float = (lod2_radius_chunks * CHUNK_SIZE) ** 2 var tile_radius_sq: float = (lod2_tile_radius * TILE_SIZE) ** 2 var prewarm_radius_sq: float = (_runtime_load_tile_radius() * TILE_SIZE) ** 2 if state.get("stream_tile", null) != null: if _state_has_applicable_quality_terrain(state, cam_pos): return 0 if _should_hide_terrain_fallback_for_quality(state, cam_pos): return -1 if state.get("baked_tile", null) != null and _tile_within_full_terrain_radius(state, cam_pos): return 0 return 3 if tile_dist_sq <= prewarm_radius_sq else -1 if not use_chunk_terrain_lods: if tile_dist_sq <= chunk_radius_sq: return 0 if tile_dist_sq <= tile_radius_sq: return 3 if prewarm_tile_lod_instances and tile_dist_sq <= prewarm_radius_sq: return 3 return -1 if tile_dist_sq <= chunk_radius_sq: return -1 if tile_dist_sq <= tile_radius_sq: return 3 return -1 func _compute_desired_tile_lod_visible(state: Dictionary, cam_pos: Vector3) -> bool: var tile_dist_sq: float = _tile_dist_sq(state, cam_pos) var chunk_radius_sq: float = (lod2_radius_chunks * CHUNK_SIZE) ** 2 var tile_radius_sq: float = (lod2_tile_radius * TILE_SIZE) ** 2 return tile_dist_sq <= maxf(chunk_radius_sq, tile_radius_sq) func _tile_within_full_terrain_radius(state: Dictionary, cam_pos: Vector3) -> bool: var radius_tiles := maxi(0, terrain_full_quality_radius_tiles) var radius_sq := (float(radius_tiles) * TILE_SIZE) ** 2 return _tile_dist_sq(state, cam_pos) <= radius_sq func _tile_within_control_splat_terrain_radius(state: Dictionary, cam_pos: Vector3) -> bool: var radius_tiles := maxi(0, terrain_control_splat_radius_tiles) var radius_sq := (float(radius_tiles) * TILE_SIZE) ** 2 return _tile_dist_sq(state, cam_pos) <= radius_sq func _tile_within_splat_terrain_radius(state: Dictionary, cam_pos: Vector3) -> bool: var radius_tiles := maxi(0, terrain_splat_radius_tiles) var radius_sq := (float(radius_tiles) * TILE_SIZE) ** 2 return _tile_dist_sq(state, cam_pos) <= radius_sq func _should_hide_terrain_fallback_for_quality(state: Dictionary, cam_pos: Vector3) -> bool: if not terrain_control_splat_hide_fallback: return false if not terrain_control_splat_primary or not terrain_control_splat_quality_enabled: return false if not bool(state.get("wanted", false)): return false var radius_tiles := maxi(0, terrain_control_splat_hide_fallback_radius_tiles) var radius_sq := (float(radius_tiles) * TILE_SIZE) ** 2 if _tile_dist_sq(state, cam_pos) > radius_sq: return false if String(state.get("quality_terrain_source", "")) == "control_splat_cache": return false var control_path := _get_terrain_control_splat_resource_path(int(state.get("tx", 0)), int(state.get("ty", 0))) return ResourceLoader.exists(control_path) func _should_prepare_full_tile_lod(state: Dictionary, cam_pos: Vector3) -> bool: if use_chunk_terrain_lods or not prewarm_tile_lod_instances: return false if state.get("stream_tile", null) != null: return false if int(state.get("tile_lod", -1)) <= 0: return false if int(state.get("prepared_tile_lod", -1)) == 0: return false var tile_dist_sq: float = _tile_dist_sq(state, cam_pos) var chunk_radius: float = float(lod2_radius_chunks) * CHUNK_SIZE var prepare_radius: float = chunk_radius + maxf(0.0, full_lod_prewarm_tiles) * TILE_SIZE return tile_dist_sq <= prepare_radius * prepare_radius func _is_tile_chunk_set_ready(state: Dictionary) -> bool: var desired_lods: Dictionary = state.get("desired_lods", {}) var current_chunks: Dictionary = state.get("chunks", {}) for chunk_id in desired_lods.keys(): if not current_chunks.has(chunk_id): return false if int(current_chunks[chunk_id].get("lod", -999)) != int(desired_lods[chunk_id]): return false return true func _tile_dist_sq(state: Dictionary, cam_pos: Vector3) -> float: var min_x: float = float(state["tx"]) * TILE_SIZE var max_x: float = min_x + TILE_SIZE var min_z: float = float(state["ty"]) * TILE_SIZE var max_z: float = min_z + TILE_SIZE var dx := 0.0 if cam_pos.x < min_x: dx = min_x - cam_pos.x elif cam_pos.x > max_x: dx = cam_pos.x - max_x var dz := 0.0 if cam_pos.z < min_z: dz = min_z - cam_pos.z elif cam_pos.z > max_z: dz = cam_pos.z - max_z return dx * dx + dz * dz func _is_tile_queued(key: String) -> bool: if _tile_loading_tasks.has(key): return true for request in _tile_load_queue: if request["key"] == key: return true return false 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("Streaming focus source is missing or is not Node3D: %s" % streaming_focus_source_path) _missing_focus_source_reported = true return _missing_focus_source_reported = false _set_streaming_focus_from_vector3(focus_source.global_position) func _set_streaming_focus_from_vector3(world_position: Vector3) -> void: var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new( world_position.x, world_position.y, world_position.z ) set_streaming_focus(STREAMING_FOCUS_SCRIPT.new(typed_world_position)) func _streaming_focus_to_vector3() -> Vector3: var world_position: GodotWorldPosition = _streaming_focus.world_position return Vector3(world_position.x_units, world_position.y_units, world_position.z_units) func _get_editor_view_camera() -> Camera3D: # EditorInterface owns this camera. This method is the explicit editor adapter; # runtime streaming never discovers a viewport camera. var editor_viewport := EditorInterface.get_editor_viewport_3d(0) if editor_viewport == null: return null return editor_viewport.get_camera_3d() func _get_auto_position_camera() -> Camera3D: if camera_path == NodePath(): return null return get_node_or_null(camera_path) as Camera3D func _can_use_baked_tile_cache() -> bool: return use_baked_tile_cache and not use_chunk_terrain_lods func _get_baked_tile_resource_path(tx: int, ty: int) -> String: return baked_tile_cache_dir.path_join(map_name).path_join("%s_%d_%d.res" % [map_name, tx, ty]) func _get_streaming_tile_resource_path(tx: int, ty: int) -> String: return streaming_tile_cache_dir.path_join(map_name).path_join("%s_%d_%d.res" % [map_name, tx, ty]) func _get_terrain_control_splat_resource_path(tx: int, ty: int) -> String: return terrain_control_splat_cache_dir.path_join(map_name).path_join("%s_%d_%d.res" % [map_name, tx, ty]) func _get_terrain_splat_resource_path(tx: int, ty: int) -> String: return terrain_splat_cache_dir.path_join(map_name).path_join("%s_%d_%d.res" % [map_name, tx, ty]) func _terrain_splat_queue_size() -> int: return _terrain_splat_cache_tasks.size() + _terrain_splat_tasks.size() + _terrain_splat_result_queue.size() func _state_has_applicable_quality_terrain(state: Dictionary, cam_pos: Vector3) -> bool: var quality_mesh: Mesh = state.get("quality_terrain_mesh", null) if quality_mesh == null: return false var quality_source := String(state.get("quality_terrain_source", "")) if quality_source == "control_splat_cache": return _tile_within_control_splat_terrain_radius(state, cam_pos) if quality_source == "splat_cache" or quality_source == "splat": return _tile_within_splat_terrain_radius(state, cam_pos) if quality_source == "baked_full": return _tile_within_full_terrain_radius(state, cam_pos) return _tile_within_full_terrain_radius(state, cam_pos) func _get_baked_tile_mesh(state: Dictionary, lod: int) -> Mesh: var stream_tile: Resource = state.get("stream_tile", null) var baked_tile: Resource = state.get("baked_tile", null) if stream_tile != null: if lod <= 0: var quality_mesh: Mesh = state.get("quality_terrain_mesh", null) if quality_mesh != null: return quality_mesh return stream_tile.get("terrain_mesh") if baked_tile == null: return null if lod <= 0: return baked_tile.get("full_mesh") return baked_tile.get("coarse_mesh") func _should_refresh_focus(focus_pos: Vector3) -> bool: if not _has_refresh_focus: return true var current_tile := _world_to_tile(focus_pos) var last_tile := _world_to_tile(_last_refresh_focus_pos) if current_tile != last_tile: return true var min_distance := maxf(1.0, streaming_update_distance) return focus_pos.distance_squared_to(_last_refresh_focus_pos) >= min_distance * min_distance func _note_refresh_focus(focus_pos: Vector3) -> void: _last_refresh_focus_pos = focus_pos _has_refresh_focus = true func _reset_refresh_focus() -> void: _last_refresh_focus_pos = Vector3.ZERO _has_refresh_focus = false func _tile_mesh_cache_key(tile_key: String, lod: int) -> String: return "%s|%d" % [tile_key, lod] func _ensure_world_wmo_root() -> void: if _world_wmo_root != null and is_instance_valid(_world_wmo_root): return _world_wmo_root = Node3D.new() _world_wmo_root.name = "WMOs" _terrain_root.add_child(_world_wmo_root) _set_editor_owner_recursive(_world_wmo_root) func _wdt_has_global_wmo() -> bool: return bool(_wdt_info.get("has_global_wmo", false)) and _wdt_info.has("global_wmo_name") func _place_global_wmo() -> void: if not _wdt_has_global_wmo(): return if _wmo_registry.has("global"): return _ensure_world_wmo_root() var rel_path := String(_wdt_info.get("global_wmo_name", "")).replace("\\", "/") if rel_path.is_empty(): return var placement: Dictionary = _wdt_info.get("global_wmo_placement", {}) if placement.is_empty(): placement = { "pos": Vector3.ZERO, "rot": Vector3.ZERO, "scale": 1.0, } var instance := _instantiate_wmo_world(rel_path, placement) if instance == null: push_warning("StreamingWorld: failed to load global WMO: %s" % rel_path) return _world_wmo_root.add_child(instance) _set_editor_owner_recursive(instance) _wmo_registry["global"] = { "node": instance, "refs": {"__global__": true}, } func _sync_detail_assets(state: Dictionary, focus_pos: Vector3) -> Dictionary: if not bool(state.get("wanted", false)): _disable_tile_detail_assets(state) return state var tile_root: Node = state.get("root", null) if tile_root == null or not is_instance_valid(tile_root): return state if enable_wmo and _tile_within_detail_radius(state, focus_pos, wmo_tile_radius): var refs: Array = state.get("wmo_refs", []) var wmo_placements: Array = state.get("wmo_placements", []) var wmo_key := String(state.get("key", "")) if refs.is_empty() and not wmo_placements.is_empty() and not _wmo_build_jobs.has(wmo_key): _enqueue_detail_asset_sync(String(state.get("key", ""))) else: _unregister_tile_wmos(state) if enable_m2_assets and _tile_within_detail_radius(state, focus_pos, m2_tile_radius): var key := String(state.get("key", "")) var m2_placements: Array = state.get("m2_placements", []) if ( not bool(state.get("m2_built", false)) and not m2_placements.is_empty() and not _m2_group_tasks.has(key) and not _m2_build_jobs.has(key) ): _enqueue_detail_asset_sync(key) else: _remove_tile_m2_assets(state) state["m2_built"] = false return state func _enqueue_detail_asset_sync(key: String) -> void: if key.is_empty() or _detail_asset_queued.has(key): return _detail_asset_queued[key] = true _detail_asset_queue.append(key) func _process_detail_asset_queue() -> void: var ops_left: int = maxi(0, detail_asset_ops_per_tick) while ops_left > 0 and not _detail_asset_queue.is_empty(): var key: String = String(_detail_asset_queue.pop_front()) _detail_asset_queued.erase(key) _process_detail_asset_key(key) ops_left -= 1 func _process_detail_asset_key(key: String) -> void: if not _tile_states.has(key): return var state: Dictionary = _tile_states[key] if not bool(state.get("wanted", false)): _disable_tile_detail_assets(state) _tile_states[key] = state return var tile_root: Node = state.get("root", null) if tile_root == null or not is_instance_valid(tile_root): return if enable_wmo and _tile_within_detail_radius(state, _last_focus_pos, wmo_tile_radius): var refs: Array = state.get("wmo_refs", []) if refs.is_empty(): _register_tile_wmos( state, state.get("wmo_names", PackedStringArray()), state.get("wmo_placements", [])) else: _unregister_tile_wmos(state) if enable_m2_assets and _tile_within_detail_radius(state, _last_focus_pos, m2_tile_radius): if not bool(state.get("m2_built", false)): var m2_request := _request_tile_m2_assets( tile_root as Node3D, String(state.get("key", "")), state.get("origin", Vector3.ZERO), state.get("m2_names", PackedStringArray()), state.get("m2_placements", [])) state["m2_built"] = bool(m2_request.get("built", false)) if m2_request.has("unique_keys"): state["m2_unique_keys"] = m2_request.get("unique_keys", []) if m2_request.has("skipped_unique_keys"): state["m2_skipped_unique_keys"] = m2_request.get("skipped_unique_keys", []) else: _remove_tile_m2_assets(state) state["m2_built"] = false _tile_states[key] = state func _disable_tile_detail_assets(state: Dictionary) -> void: _unregister_tile_wmos(state) _remove_tile_m2_assets(state) state["m2_built"] = false func _tile_within_detail_radius(state: Dictionary, focus_pos: Vector3, radius_tiles: int) -> bool: if radius_tiles < 0: return true var radius_sq := (float(radius_tiles) * TILE_SIZE) ** 2 return _tile_dist_sq(state, focus_pos) <= radius_sq func _register_tile_wmos(state: Dictionary, wmo_names: PackedStringArray, wmo_placements: Array) -> void: var refs: Array = state.get("wmo_refs", []) if wmo_names.is_empty() or wmo_placements.is_empty(): state["wmo_refs"] = refs return _ensure_world_wmo_root() var tile_key: String = state["key"] if _wmo_build_jobs.has(tile_key): state["wmo_building"] = true return _wmo_build_jobs[tile_key] = { "names": wmo_names, "placements": wmo_placements, "index": 0, "refs": refs, } _wmo_build_queue.append(tile_key) state["wmo_building"] = true func _process_wmo_build_jobs() -> void: _drain_wmo_render_loads() _drain_wmo_scene_loads() var ops_left: int = maxi(0, wmo_build_instances_per_tick) while ops_left > 0 and not _wmo_build_queue.is_empty(): var tile_key: String = String(_wmo_build_queue.front()) if not _wmo_build_jobs.has(tile_key): _wmo_build_queue.pop_front() continue if not _tile_states.has(tile_key): _wmo_build_jobs.erase(tile_key) _wmo_build_queue.pop_front() continue var state: Dictionary = _tile_states[tile_key] if not bool(state.get("wanted", false)) or not _tile_within_detail_radius(state, _last_focus_pos, wmo_tile_radius): _cancel_wmo_build_job(tile_key) _unregister_tile_wmos(state) _tile_states[tile_key] = state continue var job: Dictionary = _wmo_build_jobs[tile_key] var wmo_names: PackedStringArray = job.get("names", PackedStringArray()) var wmo_placements: Array = job.get("placements", []) var refs: Array = job.get("refs", []) var index: int = int(job.get("index", 0)) if index >= wmo_placements.size(): state["wmo_refs"] = refs state["wmo_building"] = false _tile_states[tile_key] = state _wmo_build_jobs.erase(tile_key) _wmo_build_queue.pop_front() continue var placement_variant = wmo_placements[index] var advance_job := true if placement_variant is Dictionary: var placement: Dictionary = placement_variant var name_id: int = int(placement.get("name_id", -1)) if name_id >= 0 and name_id < wmo_names.size(): var unique_key := _wmo_unique_key(placement, tile_key, index) if _wmo_registry.has(unique_key): var entry: Dictionary = _wmo_registry[unique_key] var entry_refs: Dictionary = entry["refs"] entry_refs[tile_key] = true refs.append(unique_key) else: var rel_path: String = str(wmo_names[name_id]).replace("\\", "/") var render_resource := _get_wmo_render_or_request(rel_path) if render_resource != null: var instance := _instantiate_wmo_render_root(rel_path, placement) if instance != null: _world_wmo_root.add_child(instance) _set_editor_owner_recursive(instance) _wmo_registry[unique_key] = { "node": instance, "refs": {tile_key: true}, } refs.append(unique_key) _start_wmo_render_build(unique_key, instance, render_resource) else: var normalized_rel := _normalize_wmo_rel_path(rel_path) if _wmo_render_load_requests.has(normalized_rel): advance_job = false else: var scene := _get_wmo_scene_or_request(rel_path) if scene == null: if _wmo_scene_load_requests.has(normalized_rel): advance_job = false else: var live_instance := _instantiate_wmo_world(rel_path, placement) if live_instance != null: _prepare_runtime_wmo_instance(live_instance) _world_wmo_root.add_child(live_instance) _set_editor_owner_recursive(live_instance) _wmo_registry[unique_key] = { "node": live_instance, "refs": {tile_key: true}, } refs.append(unique_key) else: var scene_instance := _instantiate_wmo_scene(rel_path, scene, placement) if scene_instance != null: _prepare_runtime_wmo_instance(scene_instance) _world_wmo_root.add_child(scene_instance) _set_editor_owner_recursive(scene_instance) _wmo_registry[unique_key] = { "node": scene_instance, "refs": {tile_key: true}, } refs.append(unique_key) if advance_job: job["refs"] = refs if not advance_job: _wmo_build_jobs[tile_key] = job state["wmo_refs"] = refs state["wmo_building"] = true _tile_states[tile_key] = state _wmo_build_queue.pop_front() _wmo_build_queue.append(tile_key) ops_left -= 1 continue job["index"] = index + 1 _wmo_build_jobs[tile_key] = job state["wmo_refs"] = refs state["wmo_building"] = true _tile_states[tile_key] = state ops_left -= 1 if int(job["index"]) >= wmo_placements.size(): state["wmo_refs"] = refs state["wmo_building"] = false _tile_states[tile_key] = state _wmo_build_jobs.erase(tile_key) _wmo_build_queue.pop_front() func _drain_wmo_scene_loads() -> void: for normalized_rel_variant in _wmo_scene_load_requests.keys(): var normalized_rel := String(normalized_rel_variant) var path := String(_wmo_scene_load_requests[normalized_rel]) var status := ResourceLoader.load_threaded_get_status(path) if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED: continue _wmo_scene_load_requests.erase(normalized_rel) if status != ResourceLoader.THREAD_LOAD_LOADED: _wmo_scene_cache_missing[normalized_rel] = true continue var resource: Resource = ResourceLoader.load_threaded_get(path) if resource is PackedScene and _is_wmo_scene_cache_current(resource as PackedScene): _wmo_scene_resource_cache[normalized_rel] = resource as PackedScene else: _wmo_scene_cache_missing[normalized_rel] = true func _drain_wmo_render_loads() -> void: for normalized_rel_variant in _wmo_render_load_requests.keys(): var normalized_rel := String(normalized_rel_variant) var path := String(_wmo_render_load_requests[normalized_rel]) var status := ResourceLoader.load_threaded_get_status(path) if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED: continue _wmo_render_load_requests.erase(normalized_rel) if status != ResourceLoader.THREAD_LOAD_LOADED: _wmo_render_missing_cache[normalized_rel] = true continue var resource: Resource = ResourceLoader.load_threaded_get(path) if ( resource != null and resource.get_script() == WMO_STREAMING_SCRIPT and int(resource.get("format_version")) >= WMO_STREAMING_SCRIPT.FORMAT_VERSION ): _wmo_render_cache[normalized_rel] = resource else: _wmo_render_missing_cache[normalized_rel] = true func _normalize_wmo_rel_path(rel_path: String) -> String: return rel_path.replace("\\", "/").to_lower() func _get_wmo_render_or_request(rel_path: String) -> Resource: var normalized_rel := _normalize_wmo_rel_path(rel_path) if normalized_rel.is_empty(): return null if _wmo_render_cache.has(normalized_rel): return _wmo_render_cache[normalized_rel] if _wmo_render_missing_cache.has(normalized_rel): return null if _wmo_render_load_requests.has(normalized_rel): return null var cache_path := wmo_render_cache_dir.path_join(normalized_rel.get_basename() + ".res") if not ResourceLoader.exists(cache_path): return null var err := ResourceLoader.load_threaded_request( cache_path, "", false, ResourceLoader.CACHE_MODE_REUSE) if err == OK or err == ERR_BUSY: _wmo_render_load_requests[normalized_rel] = cache_path return null func _get_wmo_scene_or_request(rel_path: String) -> PackedScene: var normalized_rel := _normalize_wmo_rel_path(rel_path) if normalized_rel.is_empty(): return null if _wmo_scene_resource_cache.has(normalized_rel): return _wmo_scene_resource_cache[normalized_rel] if _wmo_scene_cache_missing.has(normalized_rel): return null if _wmo_scene_load_requests.has(normalized_rel): return null var cache_path := wmo_cache_dir.path_join(normalized_rel.get_basename() + ".tscn") if not ResourceLoader.exists(cache_path): _wmo_scene_cache_missing[normalized_rel] = true return null if wmo_max_runtime_scene_mb > 0.0: var size_bytes := _get_resource_file_size(cache_path) var max_bytes := int(wmo_max_runtime_scene_mb * 1024.0 * 1024.0) if size_bytes > max_bytes: _wmo_scene_cache_missing[normalized_rel] = true if debug_streaming: print("StreamingWorld: skipping large WMO %.2f MB: %s" % [ float(size_bytes) / (1024.0 * 1024.0), normalized_rel, ]) return null var err := ResourceLoader.load_threaded_request( cache_path, "", false, ResourceLoader.CACHE_MODE_REUSE) if err == OK or err == ERR_BUSY: _wmo_scene_load_requests[normalized_rel] = cache_path else: _wmo_scene_cache_missing[normalized_rel] = true return null func _instantiate_wmo_render_root(rel_path: String, placement: Dictionary) -> Node3D: var root := Node3D.new() root.name = rel_path.get_file().get_basename() root.position = placement.get("pos", Vector3.ZERO) root.rotation = placement.get("rot", Vector3.ZERO) var scale_value: float = float(placement.get("scale", 1.0)) root.scale = Vector3.ONE * scale_value return root func _start_wmo_render_build(unique_key: String, root: Node3D, render_resource: Resource) -> void: if unique_key.is_empty() or root == null or render_resource == null: return _wmo_render_build_jobs[unique_key] = { "root": root, "resource": render_resource, "mesh_index": 0, "multimesh_index": 0, } _wmo_render_build_queue.append(unique_key) func _process_wmo_render_build_jobs() -> void: var ops_left: int = maxi(0, wmo_render_group_ops_per_tick) while ops_left > 0 and not _wmo_render_build_queue.is_empty(): var unique_key := String(_wmo_render_build_queue.front()) if not _wmo_render_build_jobs.has(unique_key): _wmo_render_build_queue.pop_front() continue var job: Dictionary = _wmo_render_build_jobs[unique_key] var root: Node = job.get("root", null) if root == null or not is_instance_valid(root): _cancel_wmo_render_build_job(unique_key) continue var render_resource: Resource = job.get("resource", null) if render_resource == null: _cancel_wmo_render_build_job(unique_key) continue var meshes: Array = render_resource.get("meshes") var mesh_transforms: Array = render_resource.get("mesh_transforms") var mesh_names: PackedStringArray = render_resource.get("mesh_names") var mesh_index: int = int(job.get("mesh_index", 0)) if mesh_index < meshes.size(): var mesh := meshes[mesh_index] as Mesh if mesh != null: _refresh_cached_wmo_mesh_materials(mesh) var mesh_instance := MeshInstance3D.new() mesh_instance.name = mesh_names[mesh_index] if mesh_index < mesh_names.size() else "Group_%d" % mesh_index mesh_instance.mesh = mesh if mesh_index < mesh_transforms.size(): mesh_instance.transform = mesh_transforms[mesh_index] mesh_instance.cast_shadow = ( GeometryInstance3D.SHADOW_CASTING_SETTING_ON if wmo_cast_shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF ) if wmo_visibility_range > 0.0: mesh_instance.visibility_range_end = wmo_visibility_range mesh_instance.visibility_range_end_margin = CHUNK_SIZE (root as Node3D).add_child(mesh_instance) _set_editor_owner_recursive(mesh_instance) job["mesh_index"] = mesh_index + 1 _wmo_render_build_jobs[unique_key] = job ops_left -= 1 continue var multimeshes: Array = render_resource.get("multimeshes") var multimesh_transforms: Array = render_resource.get("multimesh_transforms") var multimesh_names: PackedStringArray = render_resource.get("multimesh_names") var multimesh_index: int = int(job.get("multimesh_index", 0)) if multimesh_index < multimeshes.size(): var multimesh := multimeshes[multimesh_index] as MultiMesh if multimesh != null: _refresh_cached_wmo_mesh_materials(multimesh.mesh) var multimesh_instance := MultiMeshInstance3D.new() multimesh_instance.name = ( multimesh_names[multimesh_index] if multimesh_index < multimesh_names.size() else "DoodadGroup_%d" % multimesh_index ) multimesh_instance.multimesh = multimesh if multimesh_index < multimesh_transforms.size(): multimesh_instance.transform = multimesh_transforms[multimesh_index] multimesh_instance.cast_shadow = ( GeometryInstance3D.SHADOW_CASTING_SETTING_ON if wmo_cast_shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF ) if wmo_visibility_range > 0.0: multimesh_instance.visibility_range_end = wmo_visibility_range multimesh_instance.visibility_range_end_margin = CHUNK_SIZE (root as Node3D).add_child(multimesh_instance) _set_editor_owner_recursive(multimesh_instance) job["multimesh_index"] = multimesh_index + 1 _wmo_render_build_jobs[unique_key] = job ops_left -= 1 continue _cancel_wmo_render_build_job(unique_key) func _cancel_wmo_render_build_job(unique_key: String) -> void: if _wmo_render_build_jobs.has(unique_key): _wmo_render_build_jobs.erase(unique_key) _wmo_render_build_queue.erase(unique_key) func _get_resource_file_size(path: String) -> int: var file := FileAccess.open(path, FileAccess.READ) if file == null: file = FileAccess.open(ProjectSettings.globalize_path(path), FileAccess.READ) if file == null: return 0 var length := file.get_length() file.close() return int(length) func _is_wmo_scene_cache_current(scene: PackedScene) -> bool: if scene == null: return false var instance := scene.instantiate() as Node3D if instance == null: return false var current := _is_wmo_node_cache_current(instance) instance.free() return current func _is_wmo_node_cache_current(node: Node) -> bool: if node == null: return false return bool(WMO_BUILDER_SCRIPT.is_scene_cache_current(node)) func _instantiate_wmo_scene(rel_path: String, scene: PackedScene, placement: Dictionary) -> Node3D: if scene == null: return null var instance := scene.instantiate() as Node3D if instance == null: return null if not _is_wmo_node_cache_current(instance): instance.free() return null instance.name = rel_path.get_file().get_basename() instance.position = placement.get("pos", Vector3.ZERO) instance.rotation = placement.get("rot", Vector3.ZERO) var scale_value: float = float(placement.get("scale", 1.0)) instance.scale = Vector3.ONE * scale_value return instance func _prepare_runtime_wmo_instance(instance: Node3D) -> void: _refresh_cached_wmo_materials_recursive(instance) if not enable_occlusion_culling: var occluders := instance.get_node_or_null("Occluders") if occluders != null: instance.remove_child(occluders) occluders.queue_free() if wmo_cast_shadows: _apply_shadow_cast_recursive(instance, true) func _refresh_cached_wmo_materials_recursive(node: Node) -> void: if node is MeshInstance3D: _refresh_cached_wmo_mesh_materials((node as MeshInstance3D).mesh) elif node is MultiMeshInstance3D: var multimesh := (node as MultiMeshInstance3D).multimesh if multimesh != null: _refresh_cached_wmo_mesh_materials(multimesh.mesh) for child in node.get_children(): _refresh_cached_wmo_materials_recursive(child) func _refresh_cached_wmo_mesh_materials(mesh: Mesh) -> void: if mesh == null: return if int(mesh.get_meta("wow_wmo_material_refresh_version", 0)) >= WMO_MATERIAL_REFRESH_VERSION: return mesh.set_meta("wow_wmo_material_refresh_version", WMO_MATERIAL_REFRESH_VERSION) if not (mesh is ArrayMesh): return var array_mesh := mesh as ArrayMesh for surface_idx in array_mesh.get_surface_count(): var rebuilt := _rebuild_cached_wmo_material(array_mesh.surface_get_material(surface_idx)) if rebuilt != null: array_mesh.surface_set_material(surface_idx, rebuilt) func _rebuild_cached_wmo_material(material: Material) -> Material: if material == null or not material.has_meta("texture0_path"): return null var tex0_path := String(material.get_meta("texture0_path", "")) var tex1_path := String(material.get_meta("texture1_path", "")) var tex2_path := String(material.get_meta("texture2_path", "")) var textures := PackedStringArray() var tex0_index := -1 var tex1_index := -1 var tex2_index := -1 if not tex0_path.is_empty(): tex0_index = textures.size() textures.append(tex0_path) if not tex1_path.is_empty(): tex1_index = textures.size() textures.append(tex1_path) if not tex2_path.is_empty(): tex2_index = textures.size() textures.append(tex2_path) var diffuse := Color.WHITE var emissive := Color.BLACK var secondary := Color.WHITE if material is ShaderMaterial: var shader_material := material as ShaderMaterial var diffuse_value: Variant = shader_material.get_shader_parameter("diffuse_color") var emissive_value: Variant = shader_material.get_shader_parameter("emissive_color") var secondary_value: Variant = shader_material.get_shader_parameter("secondary_color") if diffuse_value is Color: diffuse = diffuse_value if emissive_value is Color: emissive = emissive_value if secondary_value is Color: secondary = secondary_value var mat_def := { "texture0": tex0_index, "texture1": tex1_index, "texture2": tex2_index, "flags": int(material.get_meta("wow_flags", 0)), "shader": int(material.get_meta("wow_shader", 0)), "blend_mode": int(material.get_meta("wow_blend_mode", 0)), "diffuse_color": diffuse, "emissive_color": emissive, "color2": secondary, } return WMO_BUILDER_SCRIPT._build_material(mat_def, textures, extracted_dir) func _adt_m2_placement_basis(rot: Vector3, rel_path: String = "") -> Basis: if not _is_waterfall_m2_path(rel_path): return _adt_m2_regular_placement_basis(rot, rel_path) # ADTLoader stores MDDF yaw as raw_yaw - 90 degrees for historical Godot # facing alignment. In our tile-positive ADT world, waterfall models need a # world-space +90 degree yaw to anchor their top to the source pool. Twist # the sheet around its own local fall axis afterwards, so the anchor stays in # place but the alpha plane is not rendered edge-on. var basis := Basis.from_euler(rot) return ( Basis(Vector3.UP, ADT_WATERFALL_WORLD_YAW_OFFSET) * basis * Basis(_waterfall_local_fall_axis(rel_path), _waterfall_sheet_twist(rel_path)) ) func _adt_m2_regular_placement_basis(rot: Vector3, rel_path: String = "") -> Basis: if not _is_elwynn_cliffrock_m2_path(rel_path): return Basis.from_euler(rot) # Elwynn cliff rock M2s are open-backed shell meshes. Plain Godot Euler # points their hollow side out of the cliff at waterfall placements. Keep the # correction model-specific so normal props retain the native ADT rotation. return ( Basis(Vector3.UP, rot.y + ADT_CLIFFROCK_WORLD_YAW_OFFSET) * Basis(Vector3.BACK, rot.x) * Basis(Vector3.RIGHT, -rot.z) ) func _adt_m2_placement_origin_offset(rot: Vector3, rel_path: String, scale_value: float) -> Vector3: if not _is_waterfall_m2_path(rel_path): return Vector3.ZERO var twist := _waterfall_sheet_twist(rel_path) if is_zero_approx(twist): return Vector3.ZERO var anchor := _waterfall_sheet_twist_anchor(rel_path) if anchor == Vector3.ZERO: return Vector3.ZERO var base := Basis(Vector3.UP, ADT_WATERFALL_WORLD_YAW_OFFSET) * Basis.from_euler(rot) var twisted := base * Basis(_waterfall_local_fall_axis(rel_path), twist) return (base * anchor - twisted * anchor) * maxf(scale_value, 0.0001) func _is_waterfall_m2_path(rel_path: String) -> bool: var name := rel_path.replace("\\", "/").to_lower().get_file().get_basename() return name == "newwaterfall" or name == "elwynntallwaterfall01" func _is_elwynn_cliffrock_m2_path(rel_path: String) -> bool: var name := rel_path.replace("\\", "/").to_lower().get_file().get_basename() return name == "elwynncliffrock01" or name == "elwynncliffrock02" func _waterfall_local_fall_axis(rel_path: String) -> Vector3: var normalized := rel_path.replace("\\", "/").to_lower() if normalized.ends_with("newwaterfall.m2") or normalized.ends_with("newwaterfall.mdx"): return Vector3(-0.138742, 0.990329, 0.0).normalized() if normalized.ends_with("elwynntallwaterfall01.m2") or normalized.ends_with("elwynntallwaterfall01.mdx"): return Vector3(-0.068962, 0.997619, 0.0).normalized() return Vector3.UP func _waterfall_sheet_twist(rel_path: String) -> float: var normalized := rel_path.replace("\\", "/").to_lower() if normalized.ends_with("newwaterfall.m2") or normalized.ends_with("newwaterfall.mdx"): return PI * 0.5 if normalized.ends_with("elwynntallwaterfall01.m2") or normalized.ends_with("elwynntallwaterfall01.mdx"): return -PI * 0.5 return 0.0 func _waterfall_sheet_twist_anchor(rel_path: String) -> Vector3: var normalized := rel_path.replace("\\", "/").to_lower() if normalized.ends_with("elwynntallwaterfall01.m2") or normalized.ends_with("elwynntallwaterfall01.mdx"): return Vector3(-2.667799, 89.62273, 0.00129) return Vector3.ZERO func _cancel_wmo_build_job(tile_key: String) -> void: if _wmo_build_jobs.has(tile_key): _wmo_build_jobs.erase(tile_key) _wmo_build_queue.erase(tile_key) func _unregister_tile_wmos(state: Dictionary) -> void: var refs: Array = state.get("wmo_refs", []) var tile_key: String = String(state.get("key", "")) if not tile_key.is_empty(): _cancel_wmo_build_job(tile_key) if refs.is_empty(): state["wmo_building"] = false return for unique_key in refs: if not _wmo_registry.has(unique_key): continue var entry: Dictionary = _wmo_registry[unique_key] var entry_refs: Dictionary = entry["refs"] entry_refs.erase(tile_key) if entry_refs.is_empty(): _cancel_wmo_render_build_job(String(unique_key)) var node: Node = entry.get("node", null) if node: node.queue_free() _wmo_registry.erase(unique_key) state["wmo_refs"] = [] state["wmo_building"] = false func _wmo_unique_key(placement: Dictionary, tile_key: String, idx: int) -> String: var uid: int = int(placement.get("unique_id", -1)) # uniqueId is always set in vanilla/3.3.5a MODF data, but older baked caches # predating the ADTLoader unique_id field will miss it. Fall back to a # per-tile synthetic key — no cross-tile dedup, but also no collisions. if uid > 0: return "uid:%d" % uid return "tile:%s:%d" % [tile_key, idx] func _clear_wmo_registry() -> void: _wmo_render_build_jobs.clear() _wmo_render_build_queue.clear() for entry_variant in _wmo_registry.values(): if not (entry_variant is Dictionary): continue var entry: Dictionary = entry_variant var node: Node = entry.get("node", null) if node and is_instance_valid(node): _queue_free_streamed_node(node) _wmo_registry.clear() if _world_wmo_root and is_instance_valid(_world_wmo_root): _queue_free_streamed_node(_world_wmo_root) _world_wmo_root = null func _request_tile_m2_assets( tile_root: Node3D, key: String, tile_origin: Vector3, m2_names: PackedStringArray, m2_placements: Array) -> Dictionary: var result := { "built": false, } if key.is_empty() or m2_names.is_empty() or m2_placements.is_empty(): return result var existing_m2_root := tile_root.get_node_or_null("M2s") if existing_m2_root != null: result["built"] = not existing_m2_root.is_queued_for_deletion() return result if _m2_group_tasks.has(key) or _m2_build_jobs.has(key): return result var filtered := _reserve_tile_m2_placements(key, m2_placements) var filtered_placements: Array = filtered.get("placements", []) result["unique_keys"] = filtered.get("unique_keys", []) result["skipped_unique_keys"] = filtered.get("skipped_unique_keys", []) if filtered_placements.is_empty(): result["built"] = true return result var task_id: int = WorkerThreadPool.add_task( _group_tile_m2_task.bind(key, tile_origin, m2_names, filtered_placements)) _m2_group_tasks[key] = task_id return result func _reserve_tile_m2_placements(tile_key: String, m2_placements: Array) -> Dictionary: var filtered_placements: Array = [] var unique_keys: Array = [] var skipped_unique_keys: Array = [] for index in m2_placements.size(): var placement_variant = m2_placements[index] if not (placement_variant is Dictionary): continue var placement: Dictionary = placement_variant var unique_key := _m2_unique_key(placement) if unique_key.is_empty(): filtered_placements.append(placement) continue if _m2_unique_registry.has(unique_key): var entry: Dictionary = _m2_unique_registry[unique_key] var owner_tile := String(entry.get("tile", "")) if owner_tile == tile_key and unique_keys.has(unique_key): continue if owner_tile != tile_key: if not skipped_unique_keys.has(unique_key): skipped_unique_keys.append(unique_key) continue _m2_unique_registry[unique_key] = {"tile": tile_key} if not unique_keys.has(unique_key): unique_keys.append(unique_key) filtered_placements.append(placement) return { "placements": filtered_placements, "unique_keys": unique_keys, "skipped_unique_keys": skipped_unique_keys, } func _m2_unique_key(placement: Dictionary) -> String: var uid: int = int(placement.get("unique_id", -1)) if uid <= 0: return "" return "uid:%d" % uid func _release_tile_m2_unique_keys(state: Dictionary, notify_candidates: bool = true) -> void: var tile_key := String(state.get("key", "")) var unique_keys: Array = state.get("m2_unique_keys", []) if tile_key.is_empty() or unique_keys.is_empty(): state["m2_unique_keys"] = [] state["m2_skipped_unique_keys"] = [] return var released_keys: Array = [] for unique_key_variant in unique_keys: var unique_key := String(unique_key_variant) if unique_key.is_empty() or not _m2_unique_registry.has(unique_key): continue var entry: Dictionary = _m2_unique_registry[unique_key] if String(entry.get("tile", "")) != tile_key: continue _m2_unique_registry.erase(unique_key) released_keys.append(unique_key) state["m2_unique_keys"] = [] state["m2_skipped_unique_keys"] = [] if not notify_candidates: return for unique_key in released_keys: _queue_m2_tiles_skipping_unique_key(String(unique_key), tile_key) func _queue_m2_tiles_skipping_unique_key(unique_key: String, released_tile_key: String) -> void: if unique_key.is_empty(): return for tile_key_variant in _tile_states.keys(): var tile_key := String(tile_key_variant) if tile_key == released_tile_key: continue var state: Dictionary = _tile_states[tile_key] var skipped_keys: Array = state.get("m2_skipped_unique_keys", []) if not skipped_keys.has(unique_key): continue if not bool(state.get("wanted", false)): continue if not _tile_within_detail_radius(state, _last_focus_pos, m2_tile_radius): continue _cancel_m2_build_job(tile_key) _remove_tile_m2_root(state) state["m2_built"] = false state["m2_skipped_unique_keys"] = [] _tile_states[tile_key] = state _enqueue_detail_asset_sync(tile_key) func _group_tile_m2_task( key: String, tile_origin: Vector3, m2_names: PackedStringArray, m2_placements: Array) -> void: var groups: Dictionary = {} for placement_variant in m2_placements: if not (placement_variant is Dictionary): continue var placement: Dictionary = placement_variant var name_id: int = int(placement.get("name_id", -1)) if name_id < 0 or name_id >= m2_names.size(): continue var normalized: String = str(m2_names[name_id]).replace("\\", "/") if normalized.ends_with(".mdx") or normalized.ends_with(".mdl"): normalized = normalized.get_basename() + ".m2" if normalized.is_empty(): continue var world_pos: Vector3 = placement.get("pos", Vector3.ZERO) var pos: Vector3 = world_pos - tile_origin var rot: Vector3 = placement.get("rot", Vector3.ZERO) var scale_value: float = float(placement.get("scale", 1.0)) var basis := _adt_m2_placement_basis(rot, normalized) var offset := _adt_m2_placement_origin_offset(rot, normalized, scale_value) var xform := Transform3D(basis.scaled(Vector3.ONE * maxf(scale_value, 0.0001)), pos + offset) if not groups.has(normalized): groups[normalized] = [] (groups[normalized] as Array).append(xform) _m2_group_result_mutex.lock() _m2_group_result_queue.append({ "key": key, "groups": groups, }) _m2_group_result_mutex.unlock() func _drain_m2_group_results() -> void: var results: Array = [] _m2_group_result_mutex.lock() if not _m2_group_result_queue.is_empty(): results = _m2_group_result_queue _m2_group_result_queue = [] _m2_group_result_mutex.unlock() for result_variant in results: if not (result_variant is Dictionary): continue var result: Dictionary = result_variant var key: String = String(result.get("key", "")) if _m2_group_tasks.has(key): WorkerThreadPool.wait_for_task_completion(int(_m2_group_tasks[key])) _m2_group_tasks.erase(key) if not _tile_states.has(key): continue var state: Dictionary = _tile_states[key] if not bool(state.get("wanted", false)): continue if not _tile_within_detail_radius(state, _last_focus_pos, m2_tile_radius): continue if bool(state.get("m2_built", false)) or _m2_build_jobs.has(key): continue var groups: Dictionary = result.get("groups", {}) if groups.is_empty(): state["m2_built"] = true _tile_states[key] = state continue var tile_root: Node = state.get("root", null) if tile_root == null or not is_instance_valid(tile_root): continue var m2_root := Node3D.new() m2_root.name = "M2s" (tile_root as Node3D).add_child(m2_root) _set_editor_owner_recursive(m2_root) _m2_build_jobs[key] = { "root": m2_root, "groups": groups, "keys": groups.keys(), "index": 0, "offset": 0, "serial": 0, } _m2_build_queue.append(key) func _drain_m2_animation_loads() -> void: for normalized_rel_variant in _m2_animation_load_requests.keys(): var normalized_rel := String(normalized_rel_variant) var pending: Dictionary = _m2_animation_load_requests[normalized_rel] var path := String(pending.get("path", "")) if path.is_empty(): _m2_animation_load_requests.erase(normalized_rel) _m2_static_animation_cache[normalized_rel] = true continue var status := ResourceLoader.load_threaded_get_status(path) if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED: continue pending["status"] = status _m2_animation_load_requests.erase(normalized_rel) _m2_animation_finalize_queue.append(pending) var ops_left: int = maxi(0, m2_animation_finalize_ops_per_tick) while ops_left > 0 and not _m2_animation_finalize_queue.is_empty(): var pending: Dictionary = _m2_animation_finalize_queue.pop_front() var normalized_rel := String(pending.get("normalized", "")) if normalized_rel.is_empty() or _m2_animated_scene_cache.has(normalized_rel) or _m2_static_animation_cache.has(normalized_rel): ops_left -= 1 continue if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED: _m2_static_animation_cache[normalized_rel] = true ops_left -= 1 continue var path := String(pending.get("path", "")) var resource: Resource = ResourceLoader.load_threaded_get(path) if resource is PackedScene: var node = (resource as PackedScene).instantiate() if node is Node3D: _repair_m2_animated_materials(normalized_rel, node as Node3D) var players := _find_animation_players_recursive(node) if not players.is_empty(): _m2_animated_scene_cache[normalized_rel] = node as Node3D if debug_streaming: print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [normalized_rel, path, players.size()]) ops_left -= 1 continue node.free() _m2_static_animation_cache[normalized_rel] = true ops_left -= 1 func _drain_m2_mesh_loads() -> void: for normalized_rel_variant in _m2_mesh_load_requests.keys(): var normalized_rel := String(normalized_rel_variant) var pending: Dictionary = _m2_mesh_load_requests[normalized_rel] var path: String = String(pending.get("path", "")) if path.is_empty(): _m2_mesh_load_requests.erase(normalized_rel) _m2_missing_cache[normalized_rel] = true continue var status := ResourceLoader.load_threaded_get_status(path) if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED: continue pending["status"] = status _m2_mesh_load_requests.erase(normalized_rel) _m2_mesh_finalize_queue.append(pending) var ops_left: int = maxi(0, m2_mesh_finalize_ops_per_tick) while ops_left > 0 and not _m2_mesh_finalize_queue.is_empty(): var pending: Dictionary = _m2_mesh_finalize_queue.pop_front() var normalized_rel := String(pending.get("normalized", "")) if normalized_rel.is_empty() or _m2_mesh_cache.has(normalized_rel): ops_left -= 1 continue if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED: _m2_missing_cache[normalized_rel] = true ops_left -= 1 continue var path := String(pending.get("path", "")) var resource: Resource = ResourceLoader.load_threaded_get(path) var mesh := _extract_first_mesh_from_m2_resource(resource) if mesh != null: _m2_mesh_cache[normalized_rel] = _prepare_m2_mesh_for_runtime(normalized_rel, mesh) else: _m2_missing_cache[normalized_rel] = true ops_left -= 1 func _process_m2_build_jobs() -> void: var ops_left: int = maxi(0, m2_build_groups_per_tick) while ops_left > 0 and not _m2_build_queue.is_empty(): var key: String = String(_m2_build_queue.front()) if not _m2_build_jobs.has(key): _m2_build_queue.pop_front() continue if not _tile_states.has(key): _cancel_m2_build_job(key) _m2_build_queue.pop_front() continue var state: Dictionary = _tile_states[key] if not bool(state.get("wanted", false)) or not _tile_within_detail_radius(state, _last_focus_pos, m2_tile_radius): _cancel_m2_build_job(key) _release_tile_m2_unique_keys(state) state["m2_built"] = false _tile_states[key] = state _m2_build_queue.pop_front() continue var job: Dictionary = _m2_build_jobs[key] var root: Node = job.get("root", null) if root == null or not is_instance_valid(root): _m2_build_jobs.erase(key) _m2_build_queue.pop_front() continue var group_keys: Array = job.get("keys", []) var index: int = int(job.get("index", 0)) if index >= group_keys.size(): _finish_m2_build_job(key) _m2_build_queue.pop_front() continue var rel_path: String = String(group_keys[index]) var normalized_rel := _normalize_m2_rel_path(rel_path) var groups: Dictionary = job.get("groups", {}) var transforms: Array = groups.get(rel_path, []) var offset: int = int(job.get("offset", 0)) var animated_prototype: Node3D = null if enable_m2_animated_instances: animated_prototype = _get_or_load_m2_native_animated_prototype(rel_path) if animated_prototype == null: animated_prototype = _get_or_load_m2_animated_prototype(rel_path) if enable_m2_animated_instances and _m2_animation_load_requests.has(normalized_rel): _m2_build_queue.pop_front() _m2_build_queue.append(key) ops_left -= 1 continue var batch_size: int = ( maxi(1, m2_animated_instances_per_tick) if animated_prototype != null else maxi(1, m2_multimesh_batch_size) ) var batch_count: int = mini(batch_size, maxi(0, transforms.size() - offset)) var serial: int = int(job.get("serial", 0)) var consumed := batch_count if batch_count > 0: if animated_prototype != null: _materialize_m2_animated_batch(root as Node3D, rel_path, animated_prototype, transforms, offset, batch_count, serial) else: var mesh := _get_m2_mesh_or_request(rel_path) if mesh == null: if not _m2_missing_cache.has(normalized_rel): _m2_build_queue.pop_front() _m2_build_queue.append(key) ops_left -= 1 continue else: _materialize_m2_group_batch(root as Node3D, rel_path, mesh, transforms, offset, batch_count, serial) job["serial"] = serial + 1 if consumed <= 0 or offset + consumed >= transforms.size(): job["index"] = index + 1 job["offset"] = 0 else: job["offset"] = offset + consumed _m2_build_jobs[key] = job ops_left -= 1 if int(job["index"]) >= group_keys.size(): _finish_m2_build_job(key) _m2_build_queue.pop_front() func _materialize_m2_group_batch( m2_root: Node3D, rel_path: String, mesh: Mesh, transforms: Array, start: int, count: int, serial: int) -> void: if transforms.is_empty() or count <= 0 or mesh == null: return var mm := MultiMesh.new() mm.transform_format = MultiMesh.TRANSFORM_3D mm.mesh = mesh mm.instance_count = count for i in count: mm.set_instance_transform(i, transforms[start + i]) var mmi := MultiMeshInstance3D.new() mmi.name = "%s_%d" % [rel_path.get_file().get_basename(), serial] mmi.multimesh = mm mmi.cast_shadow = ( GeometryInstance3D.SHADOW_CASTING_SETTING_ON if m2_cast_shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF ) if m2_visibility_range > 0.0: mmi.visibility_range_end = m2_visibility_range mmi.visibility_range_end_margin = CHUNK_SIZE m2_root.add_child(mmi) _set_editor_owner_recursive(mmi) func _materialize_m2_animated_batch( m2_root: Node3D, rel_path: String, prototype: Node3D, transforms: Array, start: int, count: int, serial: int) -> void: if transforms.is_empty() or count <= 0 or prototype == null: return var batch_root := Node3D.new() batch_root.name = "%s_anim_%d" % [rel_path.get_file().get_basename(), serial] for i in count: var instance := prototype.duplicate(Node.DUPLICATE_SIGNALS | Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS) as Node3D if instance == null: continue instance.name = "%s_%d" % [rel_path.get_file().get_basename(), start + i] instance.transform = transforms[start + i] _apply_visibility_range_recursive(instance, m2_visibility_range) _apply_shadow_cast_recursive(instance, m2_cast_shadows) _copy_m2_native_animator_data(prototype, instance) batch_root.add_child(instance) _start_m2_animations(instance, rel_path, start + i) if batch_root.get_child_count() <= 0: batch_root.queue_free() return m2_root.add_child(batch_root) _set_editor_owner_recursive(batch_root) func _start_m2_animations(root: Node, rel_path: String, instance_index: int) -> void: var phase := float(abs(("%s:%d" % [rel_path, instance_index]).hash()) % 1000) / 1000.0 var native_animators := _find_m2_native_animators_recursive(root) for animator in native_animators: if animator.has_method("prepare_runtime"): animator.prepare_runtime() animator.set_phase(phase) if debug_streaming and animator.has_method("runtime_debug_state"): var state: Dictionary = animator.runtime_debug_state() print("M2_NATIVE_ANIMATOR path=%s instance=%d prepared=%s processing=%s mesh=%s bones=%d surfaces=%d length=%.2f" % [ _normalize_m2_rel_path(rel_path), instance_index, str(state.get("prepared", false)), str(state.get("processing", false)), str(state.get("has_mesh", false)), int(state.get("bones", 0)), int(state.get("surfaces", 0)), float(state.get("length", 0.0)), ]) var players := _find_animation_players_recursive(root) for player in players: var animation_name := _choose_default_m2_animation(player, rel_path) if animation_name.is_empty(): continue for name in player.get_animation_list(): var animation := player.get_animation(name) if animation != null: animation.loop_mode = Animation.LOOP_LINEAR player.play(animation_name) var animation := player.get_animation(animation_name) if animation != null and animation.length > 0.0: player.seek(animation.length * phase, true) func _copy_m2_native_animator_data(source_root: Node, target_root: Node) -> void: var source_animators := _find_m2_native_animators_recursive(source_root) var target_animators := _find_m2_native_animators_recursive(target_root) for i in range(mini(source_animators.size(), target_animators.size())): var source := source_animators[i] var target := target_animators[i] target.mesh_instance_path = source.mesh_instance_path target.bones = source.bones target.surfaces = source.surfaces target.animation_length = source.animation_length target.playback_speed = source.playback_speed func _find_m2_native_animators_recursive(root: Node) -> Array[Node]: var result: Array[Node] = [] _collect_m2_native_animators(root, result) return result func _collect_m2_native_animators(node: Node, result: Array[Node]) -> void: if node.get_script() == M2_NATIVE_ANIMATOR_SCRIPT: result.append(node) for child in node.get_children(): _collect_m2_native_animators(child, result) func _choose_default_m2_animation(player: AnimationPlayer, rel_path: String = "") -> String: var lower := rel_path.to_lower() var candidates: Array[String] = ["Stand", "Idle", "Run", "Walk"] if lower.contains("fish"): candidates = ["Run", "Walk", "Swim", "Stand", "Idle", "Death"] elif lower.contains("eagle") or lower.contains("bird") or lower.contains("gull"): candidates = ["Run", "Walk", "Swim", "Stand", "Idle"] for candidate in candidates: if player.has_animation(candidate): return candidate var names := player.get_animation_list() for candidate in ["run", "walk", "swim", "stand", "idle", "loop"]: for name in names: if String(name).to_lower().contains(candidate): return String(name) return String(names[0]) if not names.is_empty() else "" func _find_animation_players_recursive(root: Node) -> Array[AnimationPlayer]: var result: Array[AnimationPlayer] = [] _collect_animation_players(root, result) return result func _collect_animation_players(node: Node, result: Array[AnimationPlayer]) -> void: if node is AnimationPlayer: result.append(node as AnimationPlayer) for child in node.get_children(): _collect_animation_players(child, result) func _finish_m2_build_job(key: String) -> void: if not _m2_build_jobs.has(key): return var job: Dictionary = _m2_build_jobs[key] var root: Node = job.get("root", null) var built := false if root != null and is_instance_valid(root): built = root.get_child_count() > 0 if not built: root.queue_free() if _tile_states.has(key): var state: Dictionary = _tile_states[key] state["m2_built"] = true _tile_states[key] = state _m2_build_jobs.erase(key) func _cancel_m2_build_job(key: String) -> void: if not _m2_build_jobs.has(key): return var job: Dictionary = _m2_build_jobs[key] var root: Node = job.get("root", null) if root != null and is_instance_valid(root): _queue_free_streamed_node(root) _m2_build_jobs.erase(key) func _remove_tile_m2_assets(state: Dictionary) -> void: var key := String(state.get("key", "")) if not key.is_empty(): _cancel_m2_build_job(key) _release_tile_m2_unique_keys(state) _remove_tile_m2_root(state) func _remove_tile_m2_root(state: Dictionary) -> void: var tile_root: Node = state.get("root", null) if tile_root == null or not is_instance_valid(tile_root): return var m2_root := tile_root.get_node_or_null("M2s") if m2_root: m2_root.queue_free() func _apply_visibility_range_recursive(node: Node, range_end: float) -> void: if range_end <= 0.0: return if node is GeometryInstance3D: var geometry := node as GeometryInstance3D geometry.visibility_range_end = range_end geometry.visibility_range_end_margin = CHUNK_SIZE for child in node.get_children(): _apply_visibility_range_recursive(child, range_end) func _apply_shadow_cast_recursive(node: Node, cast_shadows: bool) -> void: if node is GeometryInstance3D: var geometry := node as GeometryInstance3D geometry.cast_shadow = ( GeometryInstance3D.SHADOW_CASTING_SETTING_ON if cast_shadows else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF ) for child in node.get_children(): _apply_shadow_cast_recursive(child, cast_shadows) func _strip_occluders_recursive(node: Node) -> void: for child in node.get_children(): if child is OccluderInstance3D: node.remove_child(child) child.free() else: _strip_occluders_recursive(child) func _normalize_m2_rel_path(rel_path: String) -> String: var normalized_rel := rel_path.replace("\\", "/") if normalized_rel.ends_with(".mdx") or normalized_rel.ends_with(".mdl"): normalized_rel = normalized_rel.get_basename() + ".m2" return normalized_rel func _get_m2_mesh_or_request(rel_path: String) -> Mesh: var normalized_rel := _normalize_m2_rel_path(rel_path) if normalized_rel.is_empty(): return null if _m2_mesh_cache.has(normalized_rel): return _m2_mesh_cache[normalized_rel] if _m2_missing_cache.has(normalized_rel): return null _request_m2_mesh_load(normalized_rel) return null func _request_m2_mesh_load(normalized_rel: String) -> void: if normalized_rel.is_empty() or _m2_mesh_load_requests.has(normalized_rel): return for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".tscn", ".glb"]): if not ResourceLoader.exists(cache_res_path): continue if cache_res_path.get_extension().to_lower() == "glb" and _glb_cache_animation_schema(cache_res_path) == "pivot_prefix_v1": continue var err := ResourceLoader.load_threaded_request( cache_res_path, "", false, ResourceLoader.CACHE_MODE_REUSE) if err == OK or err == ERR_BUSY: _m2_mesh_load_requests[normalized_rel] = { "normalized": normalized_rel, "path": cache_res_path, } return break _m2_missing_cache[normalized_rel] = true func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh: if mesh == null: return null if int(mesh.get_meta("wow_m2_material_refresh_version", 0)) >= M2_MATERIAL_REFRESH_VERSION: return mesh var data := _load_m2_raw_data_for_refresh(normalized_rel) if data.is_empty() or not _m2_raw_data_needs_runtime_mesh_rebuild(normalized_rel, data): mesh.set_meta("wow_m2_material_refresh_version", M2_MATERIAL_REFRESH_VERSION) return mesh var rebuilt := _rebuild_m2_mesh_from_data(data) if rebuilt != null: return rebuilt mesh.set_meta("wow_m2_material_refresh_version", M2_MATERIAL_REFRESH_VERSION) return mesh func _prepare_m2_node_for_runtime(normalized_rel: String, root: Node3D) -> void: if root == null: return for mesh_instance in _find_mesh_instances_recursive(root): if mesh_instance == null or mesh_instance.mesh == null: continue var prepared := _prepare_m2_mesh_for_runtime(normalized_rel, mesh_instance.mesh) if prepared != null: mesh_instance.mesh = prepared func _load_m2_raw_data_for_refresh(normalized_rel: String) -> Dictionary: if normalized_rel.is_empty() or not ClassDB.class_exists("M2Loader"): return {} var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(normalized_rel)) if not FileAccess.file_exists(abs_path): return {} var loader = ClassDB.instantiate("M2Loader") if loader == null: return {} var data: Variant = loader.call("load_m2", abs_path) return data if data is Dictionary else {} func _m2_raw_data_needs_runtime_mesh_rebuild(normalized_rel: String, data: Dictionary) -> bool: if _m2_runtime_rebuild_required_cache.has(normalized_rel): return bool(_m2_runtime_rebuild_required_cache[normalized_rel]) var needs_rebuild := _m2_raw_data_has_billboards(data) or _m2_raw_data_has_uv_rotation(data) _m2_runtime_rebuild_required_cache[normalized_rel] = needs_rebuild return needs_rebuild func _m2_raw_data_has_billboards(data: Dictionary) -> bool: for batch_variant in data.get("batches", []): if not (batch_variant is Dictionary): continue var batch: Dictionary = batch_variant if bool(batch.get("has_billboard", false)) or int(batch.get("billboard_vertex_count", 0)) > 0: return true return false func _m2_raw_data_has_uv_rotation(data: Dictionary) -> bool: var transforms: Array = data.get("texture_transforms", []) if transforms.is_empty(): return false var combos: PackedInt32Array = data.get("texture_transform_combos", PackedInt32Array()) if combos.is_empty(): return false for batch_variant in data.get("batches", []): if not (batch_variant is Dictionary): continue var batch: Dictionary = batch_variant var texture_count := maxi(1, int(batch.get("texture_count", 1))) var base_combo := int(batch.get("texture_transform_combo_index", -1)) for stage in mini(texture_count, 4): var combo_index := base_combo + stage if combo_index < 0 or combo_index >= combos.size(): continue var transform_index := int(combos[combo_index]) if transform_index < 0 or transform_index >= transforms.size(): continue var transform: Dictionary = transforms[transform_index] if transforms[transform_index] is Dictionary else {} var rotation: Vector4 = transform.get("rotation", Vector4(1.0, 0.0, 0.0, 1.0)) if not rotation.is_equal_approx(Vector4(1.0, 0.0, 0.0, 1.0)): return true if absf(float(transform.get("rotation_speed", 0.0))) > 0.000001: return true return false func _rebuild_m2_mesh_from_data(data: Dictionary) -> Mesh: if data.is_empty(): return null var prototype: Node3D = M2_BUILDER_SCRIPT.build(data, extracted_dir) if prototype == null: return null var rebuilt := _find_first_mesh_recursive(prototype) prototype.free() return rebuilt func _extract_first_mesh_from_m2_resource(resource: Resource) -> Mesh: if resource is Mesh: return resource as Mesh if resource is PackedScene: var node := (resource as PackedScene).instantiate() if node == null: return null var mesh := _find_first_mesh_recursive(node) node.free() return mesh return null func _find_first_mesh_recursive(node: Node) -> Mesh: if node is MeshInstance3D: return (node as MeshInstance3D).mesh for child in node.get_children(): var mesh := _find_first_mesh_recursive(child) if mesh != null: return mesh return null func _get_or_load_m2_mesh(rel_path: String) -> Mesh: var normalized_rel := _normalize_m2_rel_path(rel_path) if _m2_mesh_cache.has(normalized_rel): return _m2_mesh_cache[normalized_rel] var prototype: Node3D = _get_or_load_m2_prototype(rel_path) if prototype == null: return null var mesh := _find_first_mesh_recursive(prototype) if mesh != null and not normalized_rel.is_empty(): mesh = _prepare_m2_mesh_for_runtime(normalized_rel, mesh) _m2_mesh_cache[normalized_rel] = mesh return mesh func _build_tile_m2_placeholders(tile_root: Node3D, tile_origin: Vector3, m2_names: PackedStringArray, m2_placements: Array) -> void: if m2_names.is_empty() or m2_placements.is_empty(): return var tree_mesh := CylinderMesh.new() tree_mesh.top_radius = 0.35 tree_mesh.bottom_radius = 0.45 tree_mesh.height = 6.0 tree_mesh.radial_segments = 6 var prop_mesh := BoxMesh.new() prop_mesh.size = Vector3(1.2, 1.2, 1.2) var tree_material := StandardMaterial3D.new() tree_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED tree_material.albedo_color = Color(0.2, 0.75, 0.2, 1.0) var prop_material := StandardMaterial3D.new() prop_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED prop_material.albedo_color = Color(0.8, 0.7, 0.35, 1.0) var tree_mm := MultiMesh.new() tree_mm.transform_format = MultiMesh.TRANSFORM_3D tree_mm.mesh = tree_mesh var prop_mm := MultiMesh.new() prop_mm.transform_format = MultiMesh.TRANSFORM_3D prop_mm.mesh = prop_mesh var tree_transforms: Array[Transform3D] = [] var prop_transforms: Array[Transform3D] = [] for placement_variant in m2_placements: if not (placement_variant is Dictionary): continue var placement: Dictionary = placement_variant var name_id: int = int(placement.get("name_id", -1)) if name_id < 0 or name_id >= m2_names.size(): continue var rel_path := str(m2_names[name_id]).to_lower() var is_tree := rel_path.contains("tree") or rel_path.contains("bush") or rel_path.contains("shrub") or rel_path.contains("plant") if m2_placeholder_only_trees and not is_tree: continue var local_pos: Vector3 = placement.get("pos", Vector3.ZERO) - tile_origin var rot: Vector3 = placement.get("rot", Vector3.ZERO) var scale_value: float = float(placement.get("scale", 1.0)) var xform_basis := _adt_m2_placement_basis(rot, rel_path).scaled(Vector3.ONE * maxf(scale_value, 0.01)) var xf := Transform3D(xform_basis, local_pos + _adt_m2_placement_origin_offset(rot, rel_path, scale_value)) if is_tree: tree_transforms.append(xf) else: prop_transforms.append(xf) var root := Node3D.new() root.name = "M2Placeholders" if not tree_transforms.is_empty(): tree_mm.instance_count = tree_transforms.size() for i in tree_transforms.size(): tree_mm.set_instance_transform(i, tree_transforms[i]) var tree_mmi := MultiMeshInstance3D.new() tree_mmi.name = "Trees" tree_mmi.multimesh = tree_mm tree_mmi.material_override = tree_material root.add_child(tree_mmi) _set_editor_owner_recursive(tree_mmi) if not prop_transforms.is_empty(): prop_mm.instance_count = prop_transforms.size() for i in prop_transforms.size(): prop_mm.set_instance_transform(i, prop_transforms[i]) var prop_mmi := MultiMeshInstance3D.new() prop_mmi.name = "Props" prop_mmi.multimesh = prop_mm prop_mmi.material_override = prop_material root.add_child(prop_mmi) _set_editor_owner_recursive(prop_mmi) if root.get_child_count() > 0: tile_root.add_child(root) _set_editor_owner_recursive(root) func _instantiate_m2(rel_path: String, placement: Dictionary, tile_origin: Vector3) -> Node3D: var prototype: Node3D = _get_or_load_m2_prototype(rel_path) if prototype == null: return null var node := prototype.duplicate() as Node3D if node == null: return null node.name = rel_path.get_file().get_basename() var local_pos: Vector3 = placement.get("pos", Vector3.ZERO) - tile_origin var rot: Vector3 = placement.get("rot", Vector3.ZERO) var scale_value: float = float(placement.get("scale", 1.0)) node.transform = Transform3D( _adt_m2_placement_basis(rot, rel_path).scaled(Vector3.ONE * maxf(scale_value, 0.0001)), local_pos + _adt_m2_placement_origin_offset(rot, rel_path, scale_value)) return node func _instantiate_wmo_world(rel_path: String, placement: Dictionary) -> Node3D: var prototype := _get_or_load_wmo_prototype(rel_path) if prototype == null: return null var instance := prototype.duplicate() instance.name = rel_path.get_file().get_basename() # WMOs are parented to _terrain_root, whose position absorbs the editor # offset — use world-space placement.pos directly (no tile_origin subtraction). instance.position = placement.get("pos", Vector3.ZERO) instance.rotation = placement.get("rot", Vector3.ZERO) var scale_value: float = float(placement.get("scale", 1.0)) instance.scale = Vector3.ONE * scale_value if not enable_occlusion_culling: var occluders := instance.get_node_or_null("Occluders") if occluders != null: instance.remove_child(occluders) occluders.queue_free() if wmo_cast_shadows: _apply_shadow_cast_recursive(instance, true) return instance func _get_or_load_m2_prototype(rel_path: String) -> Node3D: var normalized_rel := _normalize_m2_rel_path(rel_path) if normalized_rel.is_empty(): return null if _m2_scene_cache.has(normalized_rel): var cached: Node3D = _m2_scene_cache[normalized_rel] _prepare_m2_node_for_runtime(normalized_rel, cached) return cached if _m2_missing_cache.has(normalized_rel): return null # Try pre-baked cache (.tscn first, then legacy .glb) for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".tscn", ".glb"]): if not ResourceLoader.exists(cache_res_path): continue if cache_res_path.get_extension().to_lower() == "glb" and _glb_cache_animation_schema(cache_res_path) == "pivot_prefix_v1": continue var resource: Resource = load(cache_res_path) if resource is PackedScene: var node = (resource as PackedScene).instantiate() if node is Node3D: _prepare_m2_node_for_runtime(normalized_rel, node as Node3D) _m2_scene_cache[normalized_rel] = node as Node3D return node as Node3D break # Fall back to raw M2Loader if not ClassDB.class_exists("M2Loader"): _m2_missing_cache[normalized_rel] = true return null var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(normalized_rel)) if not FileAccess.file_exists(abs_path): _m2_missing_cache[normalized_rel] = true return null var loader = ClassDB.instantiate("M2Loader") var data: Dictionary = loader.call("load_m2", abs_path) if data.is_empty(): _m2_missing_cache[normalized_rel] = true return null var prototype: Node3D = M2_BUILDER_SCRIPT.build(data, extracted_dir) if prototype == null: _m2_missing_cache[normalized_rel] = true return null _m2_scene_cache[normalized_rel] = prototype return prototype func _get_or_load_m2_native_animated_prototype(rel_path: String) -> Node3D: var normalized_rel := _normalize_m2_rel_path(rel_path) if normalized_rel.is_empty() or not _is_m2_native_animation_candidate(normalized_rel): return null if _m2_animated_scene_cache.has(normalized_rel): return _m2_animated_scene_cache[normalized_rel] if _m2_static_animation_cache.has(normalized_rel): return null if not ClassDB.class_exists("M2Loader"): _m2_static_animation_cache[normalized_rel] = true return null var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(normalized_rel)) if not FileAccess.file_exists(abs_path): _m2_static_animation_cache[normalized_rel] = true return null var loader = ClassDB.instantiate("M2Loader") if loader == null: _m2_static_animation_cache[normalized_rel] = true return null if not loader.has_method("load_m2_animated"): _m2_static_animation_cache[normalized_rel] = true return null var data: Dictionary = loader.call("load_m2_animated", abs_path) var animated_surfaces: Array = data.get("animated_surfaces", []) if data.is_empty() or animated_surfaces.is_empty(): _m2_static_animation_cache[normalized_rel] = true return null var prototype: Node3D = M2_NATIVE_ANIMATED_BUILDER_SCRIPT.build(data, extracted_dir) if prototype == null or prototype.get_child_count() <= 0: _m2_static_animation_cache[normalized_rel] = true return null _m2_animated_scene_cache[normalized_rel] = prototype if debug_streaming: print("M2_NATIVE_ANIM_CACHE path=%s surfaces=%d bones=%d anim_id=%d seq=%d score=%d length=%.2f" % [ normalized_rel, animated_surfaces.size(), (data.get("bones", []) as Array).size(), int(data.get("animation_id", -1)), int(data.get("animation_sequence_index", -1)), int(data.get("animation_activity_score", 0)), float(data.get("animation_length", 0.0)), ]) return prototype func _is_m2_native_animation_candidate(normalized_rel: String) -> bool: return normalized_rel.to_lower().contains("gryphonroost") func _get_or_load_m2_animated_prototype(rel_path: String) -> Node3D: var normalized_rel := _normalize_m2_rel_path(rel_path) if normalized_rel.is_empty(): return null if _m2_animated_scene_cache.has(normalized_rel): return _m2_animated_scene_cache[normalized_rel] if _m2_static_animation_cache.has(normalized_rel): return null if _m2_animation_load_requests.has(normalized_rel): return null _request_m2_animation_load(normalized_rel) return null func _request_m2_animation_load(normalized_rel: String) -> void: if normalized_rel.is_empty() or _m2_animation_load_requests.has(normalized_rel): return var cache_res_path := _find_m2_animated_glb_cache_path(normalized_rel) if cache_res_path.is_empty(): _m2_static_animation_cache[normalized_rel] = true return var err := ResourceLoader.load_threaded_request( cache_res_path, "", false, ResourceLoader.CACHE_MODE_REUSE) if err == OK or err == ERR_BUSY: _m2_animation_load_requests[normalized_rel] = { "normalized": normalized_rel, "path": cache_res_path, } else: _m2_static_animation_cache[normalized_rel] = true func _find_m2_animated_glb_cache_path(normalized_rel: String) -> String: if not _is_m2_animation_allowed(normalized_rel): return "" if _is_m2_animation_denied(normalized_rel): return "" for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".glb"]): if not ResourceLoader.exists(cache_res_path): continue if _glb_cache_is_safe_for_runtime_animation(cache_res_path): return cache_res_path return "" func _is_m2_animation_allowed(normalized_rel: String) -> bool: if m2_animated_allowlist_patterns.is_empty(): return false var lower := normalized_rel.to_lower() for pattern in m2_animated_allowlist_patterns: var needle := String(pattern).strip_edges().to_lower() if not needle.is_empty() and lower.contains(needle): return true return false func _is_m2_animation_denied(normalized_rel: String) -> bool: var lower := normalized_rel.to_lower() for pattern in m2_animated_denylist_patterns: var needle := String(pattern).strip_edges().to_lower() if not needle.is_empty() and lower.contains(needle): return true return false func _repair_m2_animated_materials(normalized_rel: String, animated_root: Node3D) -> void: if animated_root == null: return var material_root := _get_or_load_m2_material_prototype(normalized_rel) if material_root == null: return var source_meshes := _find_mesh_instances_recursive(material_root) var target_meshes := _find_mesh_instances_recursive(animated_root) if source_meshes.is_empty() or target_meshes.is_empty(): return var fallback_material := _first_mesh_material(source_meshes) for i in target_meshes.size(): var target := target_meshes[i] if target == null or target.mesh == null: continue var source := source_meshes[mini(i, source_meshes.size() - 1)] for surface_idx in target.mesh.get_surface_count(): var material := _mesh_instance_surface_material(source, surface_idx) if material == null: material = fallback_material if material != null: target.set_surface_override_material(surface_idx, material) func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D: if normalized_rel.is_empty(): return null if _m2_scene_cache.has(normalized_rel): var cached: Node3D = _m2_scene_cache[normalized_rel] _prepare_m2_node_for_runtime(normalized_rel, cached) return cached for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".tscn"]): if not ResourceLoader.exists(cache_res_path): continue var resource: Resource = load(cache_res_path) if resource is PackedScene: var node = (resource as PackedScene).instantiate() if node is Node3D: _prepare_m2_node_for_runtime(normalized_rel, node as Node3D) _m2_scene_cache[normalized_rel] = node as Node3D return node as Node3D return null func _find_mesh_instances_recursive(root: Node) -> Array[MeshInstance3D]: var result: Array[MeshInstance3D] = [] _collect_mesh_instances(root, result) return result func _collect_mesh_instances(node: Node, result: Array[MeshInstance3D]) -> void: if node is MeshInstance3D: result.append(node as MeshInstance3D) for child in node.get_children(): _collect_mesh_instances(child, result) func _first_mesh_material(meshes: Array[MeshInstance3D]) -> Material: for mesh_instance in meshes: if mesh_instance == null or mesh_instance.mesh == null: continue for surface_idx in mesh_instance.mesh.get_surface_count(): var material := _mesh_instance_surface_material(mesh_instance, surface_idx) if material != null: return material return null func _mesh_instance_surface_material(mesh_instance: MeshInstance3D, surface_idx: int) -> Material: if mesh_instance == null or mesh_instance.mesh == null: return null if surface_idx < mesh_instance.get_surface_override_material_count(): var override_material := mesh_instance.get_surface_override_material(surface_idx) if override_material != null: return override_material if surface_idx < mesh_instance.mesh.get_surface_count(): return mesh_instance.mesh.surface_get_material(surface_idx) return null func _glb_cache_is_safe_for_runtime_animation(cache_res_path: String) -> bool: var abs_path := ProjectSettings.globalize_path(cache_res_path) if not FileAccess.file_exists(abs_path): return false var file := FileAccess.open(abs_path, FileAccess.READ) if file == null or file.get_length() < 20: return false var magic := file.get_32() var version := file.get_32() file.get_32() if magic != 0x46546c67 or version != 2: return false var json_len := int(file.get_32()) var chunk_type := file.get_32() if json_len <= 0 or chunk_type != 0x4e4f534a: return false var json_text := file.get_buffer(json_len).get_string_from_utf8() var parsed = JSON.parse_string(json_text) if not (parsed is Dictionary): return false var gltf := parsed as Dictionary var animations: Array = gltf.get("animations", []) if animations.is_empty(): return false var primitive_count := _glb_primitive_count(gltf) if m2_animated_max_primitives > 0 and primitive_count > m2_animated_max_primitives: return false var schema := _glb_animation_schema(gltf) if schema == "pivot_prefix_v1": return true # Older simple critter GLBs were generated before the schema marker existed. # Accept them only after allowlist/denylist filtering and primitive-count checks. if schema.is_empty(): return true if debug_streaming: print("M2_ANIM_REJECT schema=%s primitives=%d cache=%s" % [schema, primitive_count, cache_res_path]) return false func _glb_primitive_count(gltf: Dictionary) -> int: var count := 0 var meshes: Array = gltf.get("meshes", []) for mesh_variant in meshes: if not (mesh_variant is Dictionary): continue var primitives: Array = (mesh_variant as Dictionary).get("primitives", []) count += primitives.size() return count func _glb_cache_animation_schema(cache_res_path: String) -> String: var abs_path := ProjectSettings.globalize_path(cache_res_path) if not FileAccess.file_exists(abs_path): return "" var file := FileAccess.open(abs_path, FileAccess.READ) if file == null or file.get_length() < 20: return "" var magic := file.get_32() var version := file.get_32() file.get_32() if magic != 0x46546c67 or version != 2: return "" var json_len := int(file.get_32()) var chunk_type := file.get_32() if json_len <= 0 or chunk_type != 0x4e4f534a: return "" var parsed = JSON.parse_string(file.get_buffer(json_len).get_string_from_utf8()) if parsed is Dictionary: return _glb_animation_schema(parsed as Dictionary) return "" func _glb_animation_schema(gltf: Dictionary) -> String: var asset: Dictionary = gltf.get("asset", {}) var extras: Dictionary = asset.get("extras", {}) return String(extras.get("openwc_m2_anim_schema", "")) func _get_or_load_wmo_prototype(rel_path: String) -> Node3D: var normalized_rel := rel_path.replace("\\", "/") if normalized_rel.is_empty(): return null if _wmo_prototype_cache.has(normalized_rel): return _wmo_prototype_cache[normalized_rel] if _wmo_missing_cache.has(normalized_rel): return null # Try pre-baked .tscn cache var cache_path := wmo_cache_dir.path_join(normalized_rel.get_basename() + ".tscn") if ResourceLoader.exists(cache_path): var resource: Resource = load(cache_path) if resource is PackedScene: var node = (resource as PackedScene).instantiate() if node is Node3D and _is_wmo_node_cache_current(node): _wmo_prototype_cache[normalized_rel] = node as Node3D return node as Node3D if node is Node: (node as Node).free() # Fall back to live WMOLoader if not ClassDB.class_exists("WMOLoader"): _wmo_missing_cache[normalized_rel] = true return null var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(normalized_rel)) if not FileAccess.file_exists(abs_path): _wmo_missing_cache[normalized_rel] = true return null var loader = ClassDB.instantiate("WMOLoader") if loader == null: return null var data: Dictionary = loader.call("load_wmo", abs_path) if data.is_empty(): _wmo_missing_cache[normalized_rel] = true return null var prototype: Node3D = WMO_BUILDER_SCRIPT.build(data, extracted_dir) if prototype == null: _wmo_missing_cache[normalized_rel] = true return null _wmo_prototype_cache[normalized_rel] = prototype return prototype func _get_m2_cache_resource_path(rel_path: String, ext: String = ".tscn") -> String: var normalized := rel_path.replace("\\", "/") var stem := normalized.get_basename() return m2_cache_dir.path_join(stem + ext) func _get_m2_cache_resource_paths(rel_path: String, extensions: Array[String]) -> PackedStringArray: var normalized := rel_path.replace("\\", "/") var lower := normalized.to_lower() var stems := [ normalized.get_basename(), lower.get_basename(), normalized.get_file().get_basename(), lower.get_file().get_basename(), ] var result := PackedStringArray() for ext in extensions: for stem in stems: if stem.is_empty(): continue var path := m2_cache_dir.path_join(stem + ext) if not result.has(path): result.append(path) return result func _get_cached_tile_mesh(cache_key: String) -> Mesh: if not _tile_mesh_cache.has(cache_key): return null _touch_tile_mesh_cache(cache_key) return _tile_mesh_cache[cache_key] func _store_cached_tile_mesh(cache_key: String, mesh: Mesh) -> void: if mesh == null: return _tile_mesh_cache[cache_key] = mesh _touch_tile_mesh_cache(cache_key) _trim_tile_mesh_cache() func _touch_tile_mesh_cache(cache_key: String) -> void: _tile_mesh_cache_order.erase(cache_key) _tile_mesh_cache_order.append(cache_key) func _trim_tile_mesh_cache() -> void: var limit := maxi(0, cached_tile_mesh_limit) while _tile_mesh_cache_order.size() > limit: var oldest: String = _tile_mesh_cache_order.pop_front() _tile_mesh_cache.erase(oldest) func _remove_tile_mesh_cache_entries(tile_key: String) -> void: for lod in range(0, 4): var cache_key := _tile_mesh_cache_key(tile_key, lod) _tile_mesh_cache.erase(cache_key) _tile_mesh_cache_order.erase(cache_key) func _clear_tile_mesh_cache() -> void: _tile_mesh_cache.clear() _tile_mesh_cache_order.clear() func _restore_quality_terrain_mesh(tile_key: String, state: Dictionary) -> Dictionary: if not _terrain_quality_mesh_cache.has(tile_key): return state var cached: Dictionary = _terrain_quality_mesh_cache[tile_key] var source := String(cached.get("source", "baked_full")) if source == "control_splat_cache" or source == "splat_cache" or source == "splat": _terrain_quality_mesh_cache.erase(tile_key) _terrain_quality_mesh_cache_order.erase(tile_key) return state var mesh: Mesh = cached.get("mesh", null) if mesh == null: _terrain_quality_mesh_cache.erase(tile_key) _terrain_quality_mesh_cache_order.erase(tile_key) return state state["quality_terrain_mesh"] = mesh state["quality_terrain_source"] = source _touch_quality_terrain_mesh_cache(tile_key) return state func _store_quality_terrain_mesh(tile_key: String, mesh: Mesh, source: String) -> void: if tile_key.is_empty() or mesh == null: return if source == "control_splat_cache" or source == "splat_cache" or source == "splat": return _terrain_quality_mesh_cache[tile_key] = { "mesh": mesh, "source": source, } _touch_quality_terrain_mesh_cache(tile_key) _trim_quality_terrain_mesh_cache() func _touch_quality_terrain_mesh_cache(tile_key: String) -> void: _terrain_quality_mesh_cache_order.erase(tile_key) _terrain_quality_mesh_cache_order.append(tile_key) func _trim_quality_terrain_mesh_cache() -> void: var limit := maxi(0, terrain_quality_mesh_cache_limit) while _terrain_quality_mesh_cache_order.size() > limit: var oldest: String = _terrain_quality_mesh_cache_order.pop_front() _terrain_quality_mesh_cache.erase(oldest) func _clear_quality_terrain_mesh_cache() -> void: _terrain_quality_mesh_cache.clear() _terrain_quality_mesh_cache_order.clear() func _position_camera_over_world() -> void: if _camera_initialized: return var camera := _get_auto_position_camera() if camera == null: return var center_tx: float = (_tile_min.x + _tile_max.x) * 0.5 var center_ty: float = (_tile_min.y + _tile_max.y) * 0.5 var center := _tile_center_to_world(center_tx, center_ty) var tile_span: int = int(max(_tile_max.x - _tile_min.x + 1, _tile_max.y - _tile_min.y + 1)) var height: float = max(2000.0, float(tile_span) * TILE_SIZE * 0.18) camera.global_position = center + Vector3(0.0, height, height * 0.2) camera.look_at(center, Vector3.UP) _camera_initialized = true ## Tile (tx, ty) NW corner sits at Godot (tx*TILE_SIZE, 0, ty*TILE_SIZE). ## This matches wow_to_godot output: chunk origins ≈ (TX*TILE_SIZE, h, TY*TILE_SIZE). func _tile_center_to_world(tile_x: float, tile_y: float) -> Vector3: return Vector3( (tile_x + 0.5) * TILE_SIZE, 0.0, (tile_y + 0.5) * TILE_SIZE) func _world_to_tile(world_pos: Vector3) -> Vector2i: return Vector2i( int(floor(world_pos.x / TILE_SIZE)), int(floor(world_pos.z / TILE_SIZE))) func _tile_key(tx: int, ty: int) -> String: return "%d_%d" % [tx, ty] func _set_editor_owner_recursive(node: Node) -> void: if not Engine.is_editor_hint(): return if not editor_persist_generated_nodes: return var edited_root := get_tree().edited_scene_root if edited_root == null: return node.owner = edited_root for child in node.get_children(): _set_editor_owner_recursive(child) func _make_editor_signature() -> String: var values := [ extracted_dir, map_name, quality_preset, 1 if editor_preview_enabled else 0, editor_preview_center_x, editor_preview_center_y, editor_preview_tile_radius, 1 if editor_follow_view_camera else 0, lod0_radius_chunks, lod1_radius_chunks, lod2_radius_chunks, lod2_tile_radius, warm_tile_margin, prewarm_tile_margin, 1 if prewarm_tile_lod_instances else 0, retain_tile_margin, 1 if terrain_full_quality_enabled else 0, terrain_full_quality_radius_tiles, max_concurrent_terrain_upgrade_tasks, 1 if terrain_control_splat_quality_enabled else 0, 1 if terrain_control_splat_primary else 0, 1 if terrain_control_splat_hide_fallback else 0, terrain_control_splat_hide_fallback_radius_tiles, terrain_control_splat_radius_tiles, max_active_terrain_control_splat_tiles, terrain_control_splat_cache_dir, 1 if terrain_splat_quality_enabled else 0, terrain_splat_radius_tiles, max_active_terrain_splat_tiles, max_concurrent_terrain_splat_tasks, 1 if enable_m2_assets else 0, 1 if enable_m2_placeholders else 0, m2_multimesh_batch_size, m2_mesh_finalize_ops_per_tick, wmo_build_instances_per_tick, wmo_render_group_ops_per_tick, wmo_max_runtime_scene_mb, m2_tile_radius, wmo_tile_radius, 1 if wmo_cast_shadows else 0, m2_visibility_range, wmo_visibility_range, full_lod_prewarm_tiles, boundary_prefetch_threshold, 1 if terrain_quality_log_enabled else 0, terrain_quality_log_interval, m2_cache_dir, wmo_cache_dir, wmo_render_cache_dir, streaming_tile_cache_dir, ] var parts := PackedStringArray() for value in values: parts.append(str(value)) return "|".join(parts) func _release_tile_if_unused(key: String) -> void: if not _tile_states.has(key): return var state: Dictionary = _tile_states[key] if bool(state.get("retained", false)): _tile_states[key] = state return if not state["chunks"].is_empty(): _tile_states[key] = state return if state.get("tile_lod_node", null) != null: _tile_states[key] = state return if state.get("tile_lod_rid", RID()).is_valid(): _tile_states[key] = state return if int(state.get("tile_lod", -1)) >= 0: _tile_states[key] = state return _release_tile(key) func _tile_local_to_world_transform(state: Dictionary, local_position: Vector3) -> Transform3D: var root: Node3D = state.get("root", null) var world_pos := local_position if root: world_pos += root.global_position return Transform3D(Basis.IDENTITY, world_pos) func _create_render_instance(mesh: Mesh, world_transform: Transform3D) -> RID: if mesh == null: return RID() var world := get_world_3d() if world == null: return RID() var scenario: RID = world.scenario if not scenario.is_valid(): return RID() var instance_rid := RenderingServer.instance_create() RenderingServer.instance_set_base(instance_rid, mesh.get_rid()) RenderingServer.instance_set_scenario(instance_rid, scenario) RenderingServer.instance_set_transform(instance_rid, world_transform) RenderingServer.instance_geometry_set_cast_shadows_setting( instance_rid, RenderingServer.SHADOW_CASTING_SETTING_ON if terrain_cast_shadows else RenderingServer.SHADOW_CASTING_SETTING_OFF) return instance_rid func _free_render_instance(instance_rid: RID) -> void: if instance_rid.is_valid(): RenderingServer.free_rid(instance_rid)