Compare commits

...

6 Commits

17 changed files with 972 additions and 12 deletions
+15
View File
@@ -964,6 +964,21 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
capture path. Light/Area/Skybox DBC lookup, interpolation, fog, sun and shader capture path. Light/Area/Skybox DBC lookup, interpolation, fog, sun and shader
behavior remain unchanged; weather and indoor environment state are follow-up work. behavior remain unchanged; weather and indoor environment state are follow-up work.
## 2026-07-16 World Entity Presentation Facade
- `EntityPresentationSnapshot` contract version 1 combines session `EntityId`,
typed Godot world position, visual PackedScene path, finite yaw/scale and visibility.
- `WorldRenderFacade.present_entity()` and `remove_entity()` delegate to an explicit
scene-owned `WorldEntityPresenter`; both runtime scenes compose that service.
- The presenter owns only visual subtrees. Same-path snapshots update transform/
visibility, changed paths replace after successful instantiation, and removal
detaches plus queues the owned root for deletion.
- The local player, authoritative state, GUID mapping and asset/display selection
remain outside this service. Runtime emits no entity commands yet, so baseline
output is unchanged.
- PackedScene loading is currently synchronous on create/replace and is a documented
prototype limitation, not an accepted network-scale frame-critical path.
## Practical Rule For Future Work ## Practical Rule For Future Work
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it. If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
@@ -0,0 +1,130 @@
# M03-RND-FACADE-ENTITY-001 — World entity presentation commands
<!-- OPENWC_CLAIM:M03-RND-FACADE-ENTITY-001:sindo-main-codex:2026-07-18 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-facade-entity`
- Lease expires UTC: 2026-07-18
- Integrator: M03 milestone integrator
## Outcome
Add immutable, versioned world-entity presentation input and stable facade
commands for idempotent present/update and removal, backed by a renderer-owned
scene service with explicit entity subtree ownership.
## Non-goals
- Define authoritative entity/gameplay/network state or allocate/map identities.
- Move the local player controller/presenters behind the world-entity service.
- Implement build-12340 display IDs, equipment, animation, effects or visibility rules.
- Add background loading, pooling, MultiMesh conversion or a generic ECS/framework.
- Change streamer queues, caches, world assets, baseline visuals or target status.
## Paths
- Exclusive: `src/render/entities/entity_presentation_snapshot.gd`,
`src/render/entities/world_entity_presenter.gd`,
`src/tools/verify_world_entity_presentation.gd`,
`src/tests/scenes/world_entity_visual_replacement_fixture.tscn`,
`docs/modules/world-entity-presentation.md`, this claim
- Shared/hotspots: `src/render/world_render_facade.gd`, both runtime streaming
scenes, `src/tools/verify_world_render_facade.gd`, `docs/modules/world-renderer.md`,
`docs/modules/README.md`, `RENDER.md`, `targets/03-renderer-facade.md`
- Reused unchanged: `EntityId`, `GodotWorldPosition`, existing asset-free
character presentation model fixture
- Generated/ignored: `.godot`, native DLL, generated resources, caches and
proprietary renderer corpus
## Contracts and data
- Contract version: `EntityPresentationSnapshot.CONTRACT_VERSION = 1`
- Snapshot: valid session-local `EntityId`, typed Godot world position, visual
PackedScene path, finite yaw radians, positive finite uniform scale and visibility
- Public API: `WorldRenderFacade.present_entity`, `remove_entity`,
`entity_presentation_snapshot`
- Presenter behavior: present is idempotent by entity key; same visual updates
transform/visibility, changed visual replaces the owned subtree; remove is explicit
- Schema/cache/coordinate contracts: no persisted schema or cache change
## Dependencies
- Requires: integrated M03 environment facade master `a85e416`, M01 identity and
coordinate contracts
- Blocks: completion of the first M03 facade checklist item and later entity renderer extraction
- External state: none; tests use a repository-owned asset-free PackedScene fixture
## Verification
- Commands: cold snapshot contract; presenter lifecycle/ownership regression;
facade delegation and runtime wiring; identity/coordinate/internal-access;
renderer manifest/shutdown/planner/scheduler/focus and repository gates
- Fixtures: invalid identity/position/yaw/scale/path, spawn, idempotent update,
visual replacement, missing asset, removal, detached diagnostic snapshot and teardown
- Fidelity evidence: no runtime commands are produced yet, so existing scene output
and seven baseline plans remain unchanged
- Performance budget: constant-time entity lookup; synchronous resource load only
on first presentation/visual replacement and explicitly excluded from hot-path acceptance
## Documentation deliverables
- Inline public API docs: snapshot, facade commands and presenter lifecycle
- Module specification: new world-entity-presentation specification
- Data-flow diagram: read model through facade into owned visual subtree
- Sequence/state/dependency diagrams: present/update/replace/remove/failure/teardown
- Source map/status updates: module registry, world renderer and `RENDER.md`
## Simplicity and naming
- Important names introduced: `EntityPresentationSnapshot`, `WorldEntityPresenter`,
`present_entity`, `remove_entity`
- Simplest considered solution: one immutable full visual snapshot and one
dictionary-owned Node3D presenter below an explicit scene service
- Rejected complexity/abstractions: ECS, event bus, command hierarchy, factory
registry, pooling, async loader framework or duplicate identity registry
- Unavoidable complexity and justification: renderer must own and deterministically
replace/free visual subtrees while authoritative state remains external
- Measured optimization evidence: no optimization claim; lifecycle is asset-free tested
## Status
- State: ready-for-review
- Done: immutable contract-v1 snapshot, presenter ownership lifecycle, facade
commands, runtime composition, asset-free regressions and module documentation
- Next: M03 integrator reviews and merges `1807c3a`
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-FACADE-ENTITY-001:1807c3a -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-FACADE-ENTITY-001:606770c -->
## Handoff
- Commit: `1807c3a`
- Results: facade now exposes idempotent `present_entity`, explicit
`remove_entity` and detached diagnostics. Both runtime scenes compose a
`WorldEntityPresenter` that owns one replaceable visual subtree per valid
session `EntityId`; local player and authoritative state remain separate.
- Verification: entity `snapshot_cases=8 lifecycle_cases=14 runtime_scenes=2`;
facade `delegation=9 entity_contract=6`; identities `5/5/6/5`; character
presentation `9/10/3/1`; coordinate boundary `files=119`; internal access
`43/7/9/1`; environment/ground/terrain, manifest `7/7`, shutdown, scheduler
(`20000` iterations), planner (`250` iterations), focus, editor parse, dry-run,
coordination, documentation and diff gates passed.
- Fidelity: runtime produces no world-entity commands yet, so all seven baseline
plans and existing player/world output remain unchanged. No build-12340
display, equipment, animation, effects or visibility parity claim is added.
- External/local inputs: ignored native DLL and generated ADT scripts supported
editor/dry-run only. Missing proprietary ADT/DBC/character corpus produced the
expected degraded diagnostics.
- Remaining risks: PackedScene create/replace loads synchronously and is not an
accepted network-scale hot path; resource paths are adapter inputs rather than
stable content IDs; animation/equipment/effects and scale profiling remain open.
- Documentation updated: new world-entity-presentation API/input-output/data-flow/
lifecycle/sequence/ownership/failure/configuration/migration/diagnostic/source-map
spec; world renderer, module registry and `RENDER.md` updated.
- Integration: accepted in master merge `606770c`; post-merge entity/facade,
character, internal-access, manifest, shutdown, scheduler and planner gates passed.
+1
View File
@@ -15,6 +15,7 @@
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) | | Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) | | Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) | | Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
| World entity presentation | Implemented boundary / Prototype visuals | [`world-entity-presentation.md`](world-entity-presentation.md) |
| Streaming target planner | Implemented | [`streaming-target-planner.md`](streaming-target-planner.md) | | Streaming target planner | Implemented | [`streaming-target-planner.md`](streaming-target-planner.md) |
| Render budget scheduler | Implemented | [`render-budget-scheduler.md`](render-budget-scheduler.md) | | Render budget scheduler | Implemented | [`render-budget-scheduler.md`](render-budget-scheduler.md) |
| Network/session | Planned | [`../../targets/roadmap/03-network-and-session.md`](../../targets/roadmap/03-network-and-session.md) | | Network/session | Planned | [`../../targets/roadmap/03-network-and-session.md`](../../targets/roadmap/03-network-and-session.md) |
+254
View File
@@ -0,0 +1,254 @@
# World Entity Presentation
## Metadata
| Field | Value |
|---|---|
| Status | Implemented boundary / Prototype visuals |
| Target/work package | M03 / `M03-RND-FACADE-ENTITY-001` |
| Owners | Renderer entity-presentation boundary and visual subtree lifecycle |
| Last verified | Worktree `work/sindo-main-codex/m03-facade-entity`, 2026-07-16 |
| Profiles/capabilities | Contract version 1; asset-free scene presentation only |
## Purpose
Accept immutable session-local entity visual snapshots through
`WorldRenderFacade`, create or update renderer-owned visual subtrees idempotently,
and remove them explicitly without making the renderer authoritative for entity,
gameplay, network or content state.
## Non-goals
- Allocate `EntityId` or map it to `WowGuid`, display ID or server entry.
- Present the separately composed local player through this service.
- Define build-12340 model, equipment, animation, effects or visibility rules.
- Own gameplay/world state, packet decoding, terrain or streaming decisions.
- Provide asynchronous loading, pooling, batching, persistence or cache formats.
## Context and boundaries
```mermaid
flowchart LR
WorldState[Future authoritative WorldState] --> Adapter[Entity presentation adapter]
Adapter --> Snapshot[EntityPresentationSnapshot v1]
Snapshot --> Facade[WorldRenderFacade]
Facade --> Presenter[WorldEntityPresenter]
Asset[PackedScene visual path] --> Presenter
Presenter --> Visual[Owned Node3D visual subtree]
LocalPlayer[Local player composition] -. separate boundary .-> Viewport[Rendered world]
Visual --> Viewport
```
Allowed dependencies:
- immutable M01 `EntityId` and `GodotWorldPosition` values;
- renderer-facing Godot transform and scene APIs;
- an explicit repository/resource path selected outside the renderer;
- `WorldRenderFacade` and main-thread runtime scene composition.
Forbidden dependencies:
- gameplay reducers, intents, combat/movement authority or entity allocation;
- packet codecs, GUID interpretation, server DB access or Content Project writes;
- streamer queues/tasks/caches, terrain query or camera/input state;
- hidden autoload, SceneTree group service lookup or persisted session IDs.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `EntityPresentationSnapshot.CONTRACT_VERSION` | Contract constant | Identifies additive snapshot contract version `1` | Compile/runtime constant | Incompatible changes require migration/ADR |
| `EntityPresentationSnapshot.create(...)` | Immutable value factory | Captures identity, typed position, visual path, yaw, scale and visibility | Any thread to construct; caller-owned | Invalid value carries stable `entity_presentation_*` failure code |
| `WorldRenderFacade.present_entity(snapshot)` | Command | Idempotently creates/updates/replaces one world visual | Main thread; world session | Returns `false` for invalid snapshot, missing service or failed scene creation |
| `WorldRenderFacade.remove_entity(entity_id)` | Command | Removes one known session visual | Main thread; world session | Returns `false` for invalid or unknown identity |
| `WorldRenderFacade.entity_presentation_snapshot()` | Diagnostic query | Returns detached scalar entity inventory | Main thread; caller-owned copy | Missing/invalid service returns empty snapshot with diagnostic |
| `WorldEntityPresenter.present_entity(snapshot)` | Internal service command | Owns visual load, attach, transform update and replacement | Main thread; service lifetime | Failed replacement retains prior valid subtree |
| `WorldEntityPresenter.remove_entity(entity_id)` | Internal service command | Detaches and schedules owned subtree deletion | Main thread | Unknown identity is unchanged/false |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Valid session-local `EntityId` | Future entity registry/adapter | Snapshot/presenter index | Immutable caller reference | One runtime session |
| Input | `GodotWorldPosition` | World-state presentation adapter | Snapshot/presenter transform | Immutable caller reference | One snapshot |
| Input | PackedScene resource path | Asset-selection adapter | Presenter | Copied string; resource remains Godot-owned | Until visual replacement |
| Input | Finite yaw radians, positive scale, visibility | Presentation adapter | Presenter | Scalar copies | One snapshot |
| Output | Owned Node3D subtree | Presenter | SceneTree/viewport | Presenter child ownership | Until replacement/removal/world teardown |
| Output | Detached inventory dictionary | Presenter/facade | Diagnostics/tests | Caller-owned deep copy | One query |
Side effects:
- synchronously loads a PackedScene only for first presentation or visual-path change;
- instantiates, attaches, transforms, detaches and queues visual Node3D subtrees;
- does not mutate authoritative state, streamer queues, caches, files, DB or network;
- does not retain caller snapshots after an entity entry is replaced or removed.
## Data flow
```mermaid
flowchart TD
ReadModel[Entity visual read model] --> Validate[Validate typed immutable snapshot]
Validate -->|invalid| Reject[Return false; no scene mutation]
Validate -->|valid| Lookup[Lookup by EntityId debug key]
Lookup -->|same visual path| Transform[Update world transform and visibility]
Lookup -->|new or changed path| Instantiate[Load and instantiate Node3D]
Instantiate -->|failure| Retain[Retain previous valid visual]
Instantiate -->|success| Replace[Detach old, attach replacement]
Replace --> Transform
Transform --> Inventory[Detached diagnostics]
Remove[remove_entity] --> Detach[Detach and queue_free owned root]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Absent
Absent --> Presented: valid present + successful instantiate
Absent --> Absent: invalid/missing visual
Presented --> Presented: same-path snapshot update
Presented --> Replacing: changed visual path
Replacing --> Presented: replacement succeeds
Replacing --> Presented: replacement fails; retain old
Presented --> Absent: remove_entity
Presented --> [*]: world/service teardown
Absent --> [*]: world/service teardown
```
## Main sequence
```mermaid
sequenceDiagram
participant Adapter as World-state adapter
participant Facade as WorldRenderFacade
participant Presenter as WorldEntityPresenter
participant Loader as Godot ResourceLoader
participant Tree as SceneTree
Adapter->>Facade: present_entity(immutable snapshot)
Facade->>Presenter: present_entity(snapshot)
alt same identity and visual path
Presenter->>Tree: update transform/visibility
else new or changed visual
Presenter->>Loader: load PackedScene
Loader-->>Presenter: scene or failure
alt scene is valid Node3D
Presenter->>Tree: detach old, attach/configure new
else load/shape failure
Presenter-->>Facade: false; old visual retained
end
end
Adapter->>Facade: remove_entity(EntityId)
Facade->>Presenter: remove_entity(EntityId)
Presenter->>Tree: detach + queue_free owned root
```
## Ownership, threading and resources
- `EntityPresentationSnapshot` owns copied scalars/strings and immutable references
to caller-owned identity/position values; it owns no Node, Resource or RID.
- `WorldEntityPresenter` owns its dictionary and every visual root attached below it.
- A dictionary entry retains the latest snapshot only while the entity is presented;
diagnostics expose no Node, Resource or snapshot reference.
- Scene load, instantiation, transform mutation, replacement and removal are main-thread only.
- Successful replacement instantiates first, then detaches the previous root, so an
invalid/missing new asset cannot erase the last valid presentation.
- Parent SceneTree teardown owns attached children; `_exit_tree` drops the index so
no stale lookup survives world unload.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Invalid identity/type | Snapshot/service validation | Reject without mutation | `entity_presentation_invalid_entity_id` | Supply valid session `EntityId` |
| Non-finite position/yaw | Snapshot validation | Reject without mutation | Stable position/yaw failure code | Repair adapter read model |
| Empty path or invalid scale | Snapshot validation | Reject without mutation | Stable path/scale failure code | Supply valid visual descriptor |
| Missing/non-PackedScene asset | Resource existence/load check | New visual absent; old replacement retained | Named presenter warning | Restore asset mapping and resend snapshot |
| Non-Node3D scene root | Instance type check | Free invalid instance; retain old | Named presenter warning | Fix visual scene root |
| Unknown remove | Dictionary lookup | No mutation; return `false` | Caller command result | Reconcile registry/presentation lifecycle |
| World teardown | Scene lifecycle | SceneTree frees children; index clears | Shutdown regression | Recreate service with next world |
There is no background job to cancel. Synchronous asset load is an explicit
prototype limitation and must be replaced/budgeted before network-scale use.
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Contract version | `1` | All | No | Identity/path/position/yaw/scale/visibility full snapshot |
| Present semantics | Idempotent by `EntityId` | Runtime/tools | No | Duplicate snapshots do not duplicate roots |
| Resource loading | Synchronous on create/replace | Prototype only | No | Not accepted as frame-critical production path |
| Local player | Separate composition | Current sandbox | No | Prevents facade service from owning gameplay player root |
## Persistence, cache and migration
- No snapshot, entity key or resource instance is persisted.
- `EntityId` remains session-local and must never enter content/cache schemas.
- Resource paths are current renderer adapter inputs, not stable content identity.
- Contract version 1 is additive. Changing identity lifetime, coordinate basis or
persisted representation requires a migration and ADR.
## Diagnostics and observability
- `entity_presentation_snapshot()` reports contract version, entity count and a
sorted list of `EntityId.to_debug_key`, visual path and visibility.
- The snapshot is deep-detached by the facade and contains no Node/Resource/RID.
- Load/type failures include the visual path but never proprietary bytes.
- Future performance telemetry should measure create/replace duration and active
entity/material/RID counts before introducing pooling or batching.
## Verification
- `src/tools/verify_world_entity_presentation.gd` covers eight value-contract and
fourteen lifecycle/ownership cases with repository-owned asset-free scenes.
- `src/tools/verify_world_render_facade.gd` covers command delegation, detached
diagnostics and both runtime scene service paths.
- Identity, coordinate, renderer-internal-access, shutdown, manifest, planner,
scheduler and documentation gates remain required.
- Fidelity boundary: runtime emits no entity commands yet, so existing seven
checkpoint scenes remain unchanged. No build-12340 display/animation parity is claimed.
## Extension points
- AssetRepository/display-ID adapter that produces validated visual descriptors.
- Async/budgeted load and main-thread finalize behind the same facade commands.
- Animation/equipment/effect snapshots after build-12340 fixtures define behavior.
- Spatial batching/MultiMesh only after profiling proves per-Node overhead.
- Entity registry adapter that maps `WowGuid` to session `EntityId` outside render.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Typed entity visual input | Implemented contract | Versioned immutable validation fixtures | Asset/display mapping not implemented |
| Present/update/remove lifecycle | Implemented boundary | Asset-free create/update/replace/remove tests | Network-scale async/budget path missing |
| Deterministic ownership | Implemented | One presenter-owned root per EntityId; teardown checks | RID/material budgets remain future work |
| WoW entity visual fidelity | Planned | No claim | Requires build-12340 display/equipment/animation fixtures |
## Known gaps and risks
- Synchronous PackedScene load may hitch and is not a production hot-path design.
- Resource path is renderer adapter data, not stable authored content identity.
- Full snapshots currently cover transform/visibility only, not animation/equipment/effects.
- One Node3D per entity is unprofiled at world scale; batching is deliberately deferred.
- The local player remains separately composed until gameplay/network architecture is ready.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/entities/entity_presentation_snapshot.gd` | Immutable versioned entity visual input and validation |
| `src/render/entities/world_entity_presenter.gd` | Main-thread entity visual ownership and lifecycle |
| `src/render/world_render_facade.gd` | Stable public present/remove/diagnostic commands |
| `src/tools/verify_world_entity_presentation.gd` | Snapshot and scene lifecycle regression |
| `src/tests/scenes/character_presentation_model_fixture.tscn` | Primary asset-free visual fixture |
| `src/tests/scenes/world_entity_visual_replacement_fixture.tscn` | Replacement asset-free visual fixture |
| `src/scenes/streaming/*_streaming.tscn` | Runtime facade/service composition |
## Related decisions and references
- [`world-renderer.md`](world-renderer.md)
- [`domain-identities.md`](domain-identities.md)
- [`coordinate-mapping.md`](coordinate-mapping.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
- [`../ARCHITECTURE.md`](../ARCHITECTURE.md)
- [`../GODOT_BEST_PRACTICES.md`](../GODOT_BEST_PRACTICES.md)
+40 -6
View File
@@ -5,9 +5,9 @@
| Field | Value | | Field | Value |
|---|---| |---|---|
| Status | Partial | | Status | Partial |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; `M03-RND-FACADE-001`; `M03-RND-STREAMING-PLANNER-001`; `M03-RND-SCHEDULER-001`; `M03-RND-INTERNAL-ACCESS-GATE-001`; `M03-RND-FACADE-GROUND-QUERY-001`; `M03-RND-FACADE-ENVIRONMENT-001` | | Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; `M03-RND-FACADE-001`; `M03-RND-STREAMING-PLANNER-001`; `M03-RND-SCHEDULER-001`; `M03-RND-INTERNAL-ACCESS-GATE-001`; `M03-RND-FACADE-GROUND-QUERY-001`; `M03-RND-FACADE-ENVIRONMENT-001`; `M03-RND-FACADE-ENTITY-001` |
| Owners | Renderer workstream / milestone integrator | | Owners | Renderer workstream / milestone integrator |
| Last verified | Worktree `work/sindo-main-codex/m03-facade-environment`, 2026-07-16 | | Last verified | Worktree `work/sindo-main-codex/m03-facade-entity`, 2026-07-16 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete | | Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose ## Purpose
@@ -49,6 +49,10 @@ flowchart LR
Facade --> SkyController[WowSkyController] Facade --> SkyController[WowSkyController]
Sky[DBC sky/light data] --> SkyController Sky[DBC sky/light data] --> SkyController
SkyController --> Scene SkyController --> Scene
EntityState[Entity visual read model] --> EntitySnapshot[EntityPresentationSnapshot]
EntitySnapshot --> Facade
Facade --> EntityPresenter[WorldEntityPresenter]
EntityPresenter --> Scene
``` ```
Allowed dependencies: Allowed dependencies:
@@ -69,9 +73,9 @@ Forbidden dependencies:
## Public API ## Public API
Runtime, capture and probe callers use `WorldRenderFacade` for focus, immutable Runtime, capture and probe callers use `WorldRenderFacade` for focus, immutable
environment input, metrics and rendered-ground diagnostics. `StreamingWorldLoader` environment/entity presentation input, metrics and rendered-ground diagnostics.
and `WowSkyController` remain internal scene implementations while a later M03 `StreamingWorldLoader`, `WowSkyController` and `WorldEntityPresenter` remain
package adds entity-visual commands. internal scene implementations.
Gameplay modules and Godot `EditorPlugin` package sources are repository-gated Gameplay modules and Godot `EditorPlugin` package sources are repository-gated
from externally reading/writing loader-private queue, task, cache and tile-state fields. from externally reading/writing loader-private queue, task, cache and tile-state fields.
@@ -90,6 +94,11 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `WorldRenderFacade.apply_environment_snapshot` | Public command | Delegates exact world time to the scene-owned DBC sky controller | Main thread; snapshot remains caller-owned | Returns `false` for invalid input or missing/incompatible controller | | `WorldRenderFacade.apply_environment_snapshot` | Public command | Delegates exact world time to the scene-owned DBC sky controller | Main thread; snapshot remains caller-owned | Returns `false` for invalid input or missing/incompatible controller |
| `WorldRenderFacade.streaming_world_loader_path` | Internal adapter property | Resolves the existing implementation without exposing it to consumers | Main thread, scene lifetime | Missing method/path produces one diagnostic until recovery | | `WorldRenderFacade.streaming_world_loader_path` | Internal adapter property | Resolves the existing implementation without exposing it to consumers | Main thread, scene lifetime | Missing method/path produces one diagnostic until recovery |
| `WorldRenderFacade.world_environment_controller_path` | Internal adapter property | Resolves the existing `WowSkyController` without exposing it to consumers | Main thread, scene lifetime | Missing method/path produces one diagnostic until recovery | | `WorldRenderFacade.world_environment_controller_path` | Internal adapter property | Resolves the existing `WowSkyController` without exposing it to consumers | Main thread, scene lifetime | Missing method/path produces one diagnostic until recovery |
| `EntityPresentationSnapshot.create` | Immutable value factory | Validates contract-v1 identity, typed position, visual path, yaw, scale and visibility | Any thread; caller-owned | Stable invalid code; never mutates scene |
| `WorldRenderFacade.present_entity` | Public command | Idempotently creates/updates/replaces one session entity visual | Main thread/session | Returns false for invalid input, missing service or visual load/type failure |
| `WorldRenderFacade.remove_entity` | Public command | Removes one known session entity visual | Main thread/session | Invalid/unknown identity returns false |
| `WorldRenderFacade.entity_presentation_snapshot` | Public diagnostic query | Returns detached scalar entity inventory | Main thread/caller-owned copy | Missing/invalid service returns empty dictionary with diagnostic |
| `WorldRenderFacade.world_entity_presenter_path` | Internal adapter property | Resolves renderer-owned entity visual service | Main thread/scene lifetime | Missing methods/path produces one diagnostic until recovery |
| `camera_path` | Exported property | Camera used only by optional automatic overview positioning | Main thread, scene lifetime | Missing camera skips automatic positioning | | `camera_path` | Exported property | Camera used only by optional automatic overview positioning | Main thread, scene lifetime | Missing camera skips automatic positioning |
| `quality_preset` | Exported property | Applies renderer budgets/radii | Before/at runtime depending property | Invalid combination should be diagnosed | | `quality_preset` | Exported property | Applies renderer budgets/radii | Before/at runtime depending property | Invalid combination should be diagnosed |
| `runtime_stats_enabled` | Exported property | Enables periodic performance snapshot | Runtime mutable | Logging overhead only | | `runtime_stats_enabled` | Exported property | Enables periodic performance snapshot | Runtime mutable | Logging overhead only |
@@ -107,6 +116,8 @@ loader configuration remains transitional composition data, not a caller API.
| Input | Focus-source `NodePath` | Runtime/test scene composition | `WorldRenderFacade` source adapter | Scene-owned node, sampled position | Main thread/scene lifetime | | Input | Focus-source `NodePath` | Runtime/test scene composition | `WorldRenderFacade` source adapter | Scene-owned node, sampled position | Main thread/scene lifetime |
| Input | Ground-query `GodotWorldPosition` | Renderer probe/future adapter | `WorldRenderFacade` | Immutable caller-held value | One main-thread query | | Input | Ground-query `GodotWorldPosition` | Renderer probe/future adapter | `WorldRenderFacade` | Immutable caller-held value | One main-thread query |
| Input | `WorldEnvironmentSnapshot` | World-state/capture adapter | `WorldRenderFacade` / `WowSkyController` | Immutable caller-held value; not retained | One main-thread update | | Input | `WorldEnvironmentSnapshot` | World-state/capture adapter | `WorldRenderFacade` / `WowSkyController` | Immutable caller-held value; not retained | One main-thread update |
| Input | `EntityPresentationSnapshot` | Future world-state/preview adapter | `WorldRenderFacade` / entity presenter | Immutable caller-held full visual snapshot | One main-thread present/update |
| Input | `EntityId` removal command | Future entity registry/adapter | `WorldRenderFacade` / entity presenter | Immutable session-local value | One main-thread removal |
| Input | Map/configuration | Scene/app/editor | `StreamingWorldLoader` | Caller config, copied/read | Main thread/session | | Input | Map/configuration | Scene/app/editor | `StreamingWorldLoader` | Caller config, copied/read | Main thread/session |
| Input | WDT/ADT/M2/WMO/BLP bytes | Extracted asset repository | Native loaders/builders | Loader result owns parsed data | Worker or controlled load | | Input | WDT/ADT/M2/WMO/BLP bytes | Extracted asset repository | Native loaders/builders | Loader result owns parsed data | Worker or controlled load |
| Input | Baked resources | Cache build tools | Streamer/builders | Shared immutable resource/cache entry | Session/cache lifetime | | Input | Baked resources | Cache build tools | Streamer/builders | Shared immutable resource/cache entry | Session/cache lifetime |
@@ -118,6 +129,8 @@ loader configuration remains transitional composition data, not a caller API.
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand | | Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
| Output | `RenderedGroundSample` | `WorldRenderFacade`/loaded terrain | Renderer diagnostics | Immutable caller-held value | One query | | Output | `RenderedGroundSample` | `WorldRenderFacade`/loaded terrain | Renderer diagnostics | Immutable caller-held value | One query |
| Output | Detached rendered-ground snapshot | `WorldRenderFacade` | Terrain-height probe | Caller-owned deep copy | One query | | Output | Detached rendered-ground snapshot | `WorldRenderFacade` | Terrain-height probe | Caller-owned deep copy | One query |
| Output | Entity visual Node3D subtree | `WorldEntityPresenter` | SceneTree/viewport | Presenter-owned child | Until replace/remove/teardown |
| Output | Detached entity inventory | `WorldRenderFacade` | Diagnostics/tests | Caller-owned deep copy | One query |
| Output | Logs/diagnostics | Hitch/stat instrumentation | Logs/baseline tools | Immutable records | Periodic/frame | | Output | Logs/diagnostics | Hitch/stat instrumentation | Logs/baseline tools | Immutable records | Periodic/frame |
| Test output | Diagnostic spawn marker and cold/warm PNGs | Checkpoint capture tool | Human/integrator review | SceneTree marker; `user://` files | One capture process | | Test output | Diagnostic spawn marker and cold/warm PNGs | Checkpoint capture tool | Human/integrator review | SceneTree marker; `user://` files | One capture process |
@@ -160,6 +173,11 @@ flowchart TD
RF --> SkyController[WowSkyController] RF --> SkyController[WowSkyController]
DBC[Light/Area/Skybox DBC] --> SkyController DBC[Light/Area/Skybox DBC] --> SkyController
SkyController --> World SkyController --> World
EntityReadModel[Entity visual read model] --> EntitySnapshot[EntityPresentationSnapshot]
EntitySnapshot --> RF
RF --> EntityPresenter[WorldEntityPresenter]
EntityAsset[PackedScene path] --> EntityPresenter
EntityPresenter --> World
Terrain --> World[Godot world] Terrain --> World[Godot world]
M2 --> World M2 --> World
WMO --> World WMO --> World
@@ -218,6 +236,11 @@ sequenceDiagram
Source->>Facade: apply_environment_snapshot(exact time) Source->>Facade: apply_environment_snapshot(exact time)
Facade->>Render: delegate to WowSkyController Facade->>Render: delegate to WowSkyController
Render->>Render: freeze local clock; retain DBC sampling/apply rules Render->>Render: freeze local clock; retain DBC sampling/apply rules
Source->>Facade: present_entity(full visual snapshot)
Facade->>Render: delegate to WorldEntityPresenter
Render->>Render: create/update/replace owned entity root
Source->>Facade: remove_entity(EntityId)
Facade->>Render: detach and queue_free owned root
Stream->>Budget: shutdown: cancel permit issuance Stream->>Budget: shutdown: cancel permit issuance
Stream->>Worker: shutdown: wait for WorkerThreadPool tasks Stream->>Worker: shutdown: wait for WorkerThreadPool tasks
Stream->>Stream: shutdown: finish registered ResourceLoader requests Stream->>Stream: shutdown: finish registered ResourceLoader requests
@@ -236,6 +259,10 @@ sequenceDiagram
into its existing fixed-clock fields on the main thread. into its existing fixed-clock fields on the main thread.
- `WowSkyController` continues to own DBC sampling state and mutations of its - `WowSkyController` continues to own DBC sampling state and mutations of its
duplicated Godot `Environment`, sun, skybox node and world shader globals. duplicated Godot `Environment`, sun, skybox node and world shader globals.
- `WorldEntityPresenter` owns one optional visual Node3D subtree per presented
`EntityId`; it owns no authoritative entity state and excludes the local player.
- Entity presentation snapshots remain caller-owned inputs. Detached diagnostics
expose only identity debug keys, paths and visibility, never Node/Resource/RID.
- Rendered-ground query results and diagnostic snapshots are caller-owned values; - Rendered-ground query results and diagnostic snapshots are caller-owned values;
the facade never returns Mesh, Node, tile-state or queue references. the facade never returns Mesh, Node, tile-state or queue references.
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay - Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
@@ -264,6 +291,9 @@ sequenceDiagram
| Invalid metrics result | Facade return-type validation | Return empty caller snapshot | Facade error | Restore `Dictionary` implementation contract | | Invalid metrics result | Facade return-type validation | Return empty caller snapshot | Facade error | Restore `Dictionary` implementation contract |
| Invalid environment time | Snapshot finite-value validation | Reject update; retain prior sky clock | `environment_time_not_finite` on invalid value | Supply a finite time snapshot | | Invalid environment time | Snapshot finite-value validation | Reject update; retain prior sky clock | `environment_time_not_finite` on invalid value | Supply a finite time snapshot |
| Missing environment controller | Facade path/method validation | Reject environment update; other facade APIs remain available | One facade diagnostic until recovery | Fix scene composition path/implementation | | Missing environment controller | Facade path/method validation | Reject environment update; other facade APIs remain available | One facade diagnostic until recovery | Fix scene composition path/implementation |
| Invalid entity visual snapshot | Contract validation | Reject without scene mutation | Stable `entity_presentation_*` failure code | Repair adapter read model and resend |
| Missing/invalid entity visual asset | Resource existence/load/root check | New entity absent or prior replacement retained | Named presenter warning | Restore asset mapping and resend snapshot |
| Missing entity presenter | Facade path/method validation | Reject entity commands; other facade APIs remain available | One facade diagnostic until recovery | Fix runtime scene composition |
| No focus supplied | Refresh contract | No target recomputation; queues continue draining | `refresh_streaming_focus()` returns `false` | Supply player/editor/capture focus | | No focus supplied | Refresh contract | No target recomputation; queues continue draining | `refresh_streaming_focus()` returns `false` | Supply player/editor/capture focus |
| Cache version mismatch | Required format constant | Reject stale cache/fallback | Version diagnostic | Rebuild matching cache | | Cache version mismatch | Required format constant | Reject stale cache/fallback | Version diagnostic | Rebuild matching cache |
| Worker failure | Task result/empty data | Skip/degrade affected asset | Queue/task warning | Retry/rebuild source | | Worker failure | Task result/empty data | Skip/degrade affected asset | Queue/task warning | Retry/rebuild source |
@@ -344,8 +374,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| Character presentation | Prototype | Character creator/player experiment | Full customization/equipment states | | Character presentation | Prototype | Character creator/player experiment | Full customization/equipment states |
| Camera-independent streaming focus | Implemented | Typed contract, two player-focused runtime scenes and three explicitly camera-focused diagnostic tools | Multi-focus/direction/velocity belong to planner extraction | | Camera-independent streaming focus | Implemented | Typed contract, two player-focused runtime scenes and three explicitly camera-focused diagnostic tools | Multi-focus/direction/velocity belong to planner extraction |
| Golden server-spawn visualization | Implemented | Pinned AzerothCore fixture, validated renderer manifest and diagnostic origin-marker capture at ADT `(32,48)`/MCNK `(3,12)` | No TrinityCore row or original-client visual-parity claim | | Golden server-spawn visualization | Implemented | Pinned AzerothCore fixture, validated renderer manifest and diagnostic origin-marker capture at ADT `(32,48)`/MCNK `(3,12)` | No TrinityCore row or original-client visual-parity claim |
| Stable renderer facade | Partial | Facade contracts cover focus, environment time, metrics and loaded-mesh ground query | Entity visuals remain | | Stable renderer facade | Implemented boundary / Partial fidelity | Facade contracts cover focus, environment, entity visuals, metrics and loaded-mesh ground query | Production consumers and fidelity remain |
| Environment snapshot input | Implemented boundary / Partial fidelity | Immutable time contract, facade/controller delegation and checkpoint migration | Weather/indoor state and paired zone-transition fidelity remain | | Environment snapshot input | Implemented boundary / Partial fidelity | Immutable time contract, facade/controller delegation and checkpoint migration | Weather/indoor state and paired zone-transition fidelity remain |
| Entity presentation commands | Implemented boundary / Prototype visuals | Versioned typed snapshot plus present/remove lifecycle and runtime service composition | Display/equipment/animation/effects and async scale path remain |
| Pure streaming target planner | Implemented | `M03-RND-STREAMING-PLANNER-001` asset-free contract/performance regression | Queue scheduling and loaded-tile LOD application remain in streamer | | Pure streaming target planner | Implemented | `M03-RND-STREAMING-PLANNER-001` asset-free contract/performance regression | Queue scheduling and loaded-tile LOD application remain in streamer |
| Bounded render budget scheduler | Implemented | `M03-RND-SCHEDULER-001` asset-free permit/cancellation/performance regression | Queue storage, worker concurrency and operation execution remain in streamer/services | | Bounded render budget scheduler | Implemented | `M03-RND-SCHEDULER-001` asset-free permit/cancellation/performance regression | Queue storage, worker concurrency and operation execution remain in streamer/services |
| Gameplay/editor/tool internal-access boundary | Implemented | Access gate scans gameplay, EditorPlugin packages and terrain probe against actual private streamer declarations | Add future renderer diagnostic tools explicitly to the protected list | | Gameplay/editor/tool internal-access boundary | Implemented | Access gate scans gameplay, EditorPlugin packages and terrain probe against actual private streamer declarations | Add future renderer diagnostic tools explicitly to the protected list |
@@ -380,6 +411,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|---|---| |---|---|
| `src/render/world_render_facade.gd` | Stable caller boundary for typed focus/environment, detached metrics and rendered-ground queries | | `src/render/world_render_facade.gd` | Stable caller boundary for typed focus/environment, detached metrics and rendered-ground queries |
| `src/render/environment/world_environment_snapshot.gd` | Immutable normalized world time input with explicit invalid state | | `src/render/environment/world_environment_snapshot.gd` | Immutable normalized world time input with explicit invalid state |
| `src/render/entities/entity_presentation_snapshot.gd` | Immutable contract-v1 entity visual input and validation |
| `src/render/entities/world_entity_presenter.gd` | Main-thread entity visual subtree ownership and lifecycle |
| `src/render/terrain/rendered_ground_sample.gd` | Immutable renderer-owned available/unavailable ground query result | | `src/render/terrain/rendered_ground_sample.gd` | Immutable renderer-owned available/unavailable ground query result |
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation | | `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy | | `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
@@ -390,6 +423,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/tools/verify_streaming_focus.gd` | Headless contract, dependency and runtime/tool wiring regression | | `src/tools/verify_streaming_focus.gd` | Headless contract, dependency and runtime/tool wiring regression |
| `src/tools/verify_world_render_facade.gd` | Focus/environment/metrics/ground delegation, snapshot isolation and consumer wiring regression | | `src/tools/verify_world_render_facade.gd` | Focus/environment/metrics/ground delegation, snapshot isolation and consumer wiring regression |
| `src/tools/verify_world_environment_snapshot.gd` | Cold-start environment value normalization and invalid-input regression | | `src/tools/verify_world_environment_snapshot.gd` | Cold-start environment value normalization and invalid-input regression |
| `src/tools/verify_world_entity_presentation.gd` | Entity snapshot, create/update/replace/remove and ownership regression |
| `src/tools/verify_rendered_ground_sample.gd` | Cold-start renderer ground result invariant regression | | `src/tools/verify_rendered_ground_sample.gd` | Cold-start renderer ground result invariant regression |
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression | | `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression | | `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
@@ -0,0 +1,104 @@
class_name EntityPresentationSnapshot
extends RefCounted
## Immutable renderer input for one session-local world entity visual.
## Gameplay authority, identity allocation and asset selection remain external.
const CONTRACT_VERSION := 1
const SCRIPT_PATH := "res://src/render/entities/entity_presentation_snapshot.gd"
const ENTITY_ID_SCRIPT := preload("res://src/domain/identity/entity_id.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
var is_valid: bool:
get:
return _failure_code.is_empty()
var failure_code: StringName:
get:
return _failure_code
var entity_id: RefCounted:
get:
return _entity_id
var world_position: RefCounted:
get:
return _world_position
var visual_scene_path: String:
get:
return _visual_scene_path
var yaw_radians: float:
get:
return _yaw_radians
var uniform_scale: float:
get:
return _uniform_scale
var is_visible: bool:
get:
return _is_visible
var _failure_code: StringName
var _entity_id: RefCounted
var _world_position: RefCounted
var _visual_scene_path: String
var _yaw_radians: float
var _uniform_scale: float
var _is_visible: bool
## Creates a version-1 full visual snapshot. Invalid values remain inspectable and
## are rejected without scene mutation by the facade/presenter.
static func create(
entity_id_value: RefCounted,
world_position_value: RefCounted,
visual_scene_path_value: String,
yaw_radians_value: float = 0.0,
uniform_scale_value: float = 1.0,
is_visible_value: bool = true
) -> RefCounted:
return load(SCRIPT_PATH).new(
entity_id_value,
world_position_value,
visual_scene_path_value,
yaw_radians_value,
uniform_scale_value,
is_visible_value
)
func _init(
entity_id_value: RefCounted,
world_position_value: RefCounted,
visual_scene_path_value: String,
yaw_radians_value: float,
uniform_scale_value: float,
is_visible_value: bool
) -> void:
_entity_id = entity_id_value
_world_position = world_position_value
_visual_scene_path = visual_scene_path_value.strip_edges()
_yaw_radians = yaw_radians_value
_uniform_scale = uniform_scale_value
_is_visible = is_visible_value
_failure_code = _validate()
func _validate() -> StringName:
if _entity_id == null or _entity_id.get_script() != ENTITY_ID_SCRIPT or not bool(_entity_id.call("is_valid")):
return &"entity_presentation_invalid_entity_id"
if _world_position == null or _world_position.get_script() != GODOT_WORLD_POSITION_SCRIPT:
return &"entity_presentation_invalid_world_position"
for coordinate_name in [&"x_units", &"y_units", &"z_units"]:
if not is_finite(float(_world_position.get(coordinate_name))):
return &"entity_presentation_position_not_finite"
if _visual_scene_path.is_empty():
return &"entity_presentation_visual_path_empty"
if not is_finite(_yaw_radians):
return &"entity_presentation_yaw_not_finite"
if not is_finite(_uniform_scale) or _uniform_scale <= 0.0:
return &"entity_presentation_scale_invalid"
return &""
@@ -0,0 +1 @@
uid://djvnorls0yf06
@@ -0,0 +1,130 @@
@tool
class_name WorldEntityPresenter
extends Node3D
## Main-thread owner of visual subtrees for session-local world entities.
## The full immutable snapshot is reapplied idempotently by entity identity.
const ENTITY_PRESENTATION_SNAPSHOT_SCRIPT := preload("res://src/render/entities/entity_presentation_snapshot.gd")
const ENTITY_ID_SCRIPT := preload("res://src/domain/identity/entity_id.gd")
var _presented_entities: Dictionary = {}
## Creates or updates one entity visual. A changed visual path is instantiated
## successfully before the previous subtree is detached, so failed replacement
## retains the last valid presentation.
func present_entity(entity_presentation_snapshot: RefCounted) -> bool:
if not _is_valid_snapshot(entity_presentation_snapshot):
return false
var entity_id: RefCounted = entity_presentation_snapshot.get("entity_id")
var entity_key := _entity_key(entity_id)
var visual_scene_path := String(entity_presentation_snapshot.get("visual_scene_path"))
var current_entry: Dictionary = _presented_entities.get(entity_key, {})
var visual_root := current_entry.get("visual_root") as Node3D
if visual_root == null or not is_instance_valid(visual_root) or String(current_entry.get("visual_scene_path", "")) != visual_scene_path:
var replacement_root := _instantiate_visual_root(entity_id, visual_scene_path)
if replacement_root == null:
return false
if visual_root != null and is_instance_valid(visual_root):
remove_child(visual_root)
add_child(replacement_root)
_apply_snapshot_transform(replacement_root, entity_presentation_snapshot)
if visual_root != null and is_instance_valid(visual_root):
visual_root.queue_free()
visual_root = replacement_root
else:
_apply_snapshot_transform(visual_root, entity_presentation_snapshot)
_presented_entities[entity_key] = {
"visual_root": visual_root,
"visual_scene_path": visual_scene_path,
"snapshot": entity_presentation_snapshot,
}
return true
## Removes one owned entity visual. Returns false for an invalid/unknown identity.
func remove_entity(entity_id: RefCounted) -> bool:
if entity_id == null or entity_id.get_script() != ENTITY_ID_SCRIPT or not bool(entity_id.call("is_valid")):
return false
var entity_key := _entity_key(entity_id)
if not _presented_entities.has(entity_key):
return false
var entry: Dictionary = _presented_entities[entity_key]
var visual_root := entry.get("visual_root") as Node3D
_presented_entities.erase(entity_key)
if visual_root != null and is_instance_valid(visual_root):
remove_child(visual_root)
visual_root.queue_free()
return true
## Returns detached scalar diagnostics without exposing owned Nodes or snapshots.
func entity_presentation_snapshot() -> Dictionary:
var entities: Array[Dictionary] = []
var sorted_entity_keys: Array = _presented_entities.keys()
sorted_entity_keys.sort()
for entity_key_variant in sorted_entity_keys:
var entity_key := String(entity_key_variant)
var entry: Dictionary = _presented_entities[entity_key]
var snapshot: RefCounted = entry.get("snapshot")
entities.append({
"entity_key": entity_key,
"visual_scene_path": String(entry.get("visual_scene_path", "")),
"visible": bool(snapshot.get("is_visible")),
})
return {
"contract_version": ENTITY_PRESENTATION_SNAPSHOT_SCRIPT.CONTRACT_VERSION,
"entity_count": entities.size(),
"entities": entities,
}
func _exit_tree() -> void:
_presented_entities.clear()
func _is_valid_snapshot(entity_presentation_snapshot: RefCounted) -> bool:
return (
entity_presentation_snapshot != null
and entity_presentation_snapshot.get_script() == ENTITY_PRESENTATION_SNAPSHOT_SCRIPT
and bool(entity_presentation_snapshot.get("is_valid"))
)
func _entity_key(entity_id: RefCounted) -> String:
return String(entity_id.call("to_debug_key"))
func _instantiate_visual_root(entity_id: RefCounted, visual_scene_path: String) -> Node3D:
if not ResourceLoader.exists(visual_scene_path, "PackedScene"):
push_warning("WorldEntityPresenter cannot find visual scene: %s" % visual_scene_path)
return null
var visual_scene := load(visual_scene_path) as PackedScene
if visual_scene == null:
push_warning("WorldEntityPresenter cannot load visual scene: %s" % visual_scene_path)
return null
var visual_instance := visual_scene.instantiate()
if not (visual_instance is Node3D):
visual_instance.free()
push_warning("WorldEntityPresenter visual scene root is not Node3D: %s" % visual_scene_path)
return null
var visual_root := visual_instance as Node3D
visual_root.name = "EntityVisual_%020d" % int(entity_id.get("session_sequence"))
return visual_root
func _apply_snapshot_transform(visual_root: Node3D, entity_presentation_snapshot: RefCounted) -> void:
var world_position: RefCounted = entity_presentation_snapshot.get("world_position")
var position_units := Vector3(
float(world_position.get("x_units")),
float(world_position.get("y_units")),
float(world_position.get("z_units"))
)
var uniform_scale := float(entity_presentation_snapshot.get("uniform_scale"))
var yaw_radians := float(entity_presentation_snapshot.get("yaw_radians"))
visual_root.global_transform = Transform3D(
Basis(Vector3.UP, yaw_radians).scaled(Vector3.ONE * uniform_scale),
position_units
)
visual_root.visible = bool(entity_presentation_snapshot.get("is_visible"))
@@ -0,0 +1 @@
uid://b0keiyi1wuu5i
+64
View File
@@ -9,6 +9,8 @@ const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd") const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
const RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd") const RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
const WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT := preload("res://src/render/environment/world_environment_snapshot.gd") const WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT := preload("res://src/render/environment/world_environment_snapshot.gd")
const ENTITY_PRESENTATION_SNAPSHOT_SCRIPT := preload("res://src/render/entities/entity_presentation_snapshot.gd")
const ENTITY_ID_SCRIPT := preload("res://src/domain/identity/entity_id.gd")
## Internal renderer implementation resolved relative to this node. ## Internal renderer implementation resolved relative to this node.
## The referenced node remains owned by its scene parent. ## The referenced node remains owned by its scene parent.
@@ -18,14 +20,19 @@ const WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT := preload("res://src/render/environment
## The facade delegates snapshots but owns neither the controller nor Environment. ## The facade delegates snapshots but owns neither the controller nor Environment.
@export var world_environment_controller_path: NodePath = NodePath("../WowSkyController") @export var world_environment_controller_path: NodePath = NodePath("../WowSkyController")
## Scene-owned service that creates and owns session-local world entity visuals.
@export var world_entity_presenter_path: NodePath = NodePath("../WorldEntityPresenter")
## Optional explicit Node3D source sampled in Godot world coordinates. ## Optional explicit Node3D source sampled in Godot world coordinates.
## Runtime scenes use the player; capture and probe tools replace it with their camera. ## Runtime scenes use the player; capture and probe tools replace it with their camera.
@export var streaming_focus_source_path: NodePath @export var streaming_focus_source_path: NodePath
var _streaming_world_loader: Node var _streaming_world_loader: Node
var _world_environment_controller: Node var _world_environment_controller: Node
var _world_entity_presenter: Node
var _missing_loader_reported := false var _missing_loader_reported := false
var _missing_environment_controller_reported := false var _missing_environment_controller_reported := false
var _missing_entity_presenter_reported := false
var _missing_focus_source_reported := false var _missing_focus_source_reported := false
@@ -116,6 +123,43 @@ func apply_environment_snapshot(world_environment_snapshot: RefCounted) -> bool:
return bool(controller.call("apply_environment_snapshot", world_environment_snapshot)) return bool(controller.call("apply_environment_snapshot", world_environment_snapshot))
## Idempotently creates or updates one world entity visual through the renderer-
## owned presenter. The immutable full snapshot remains caller-owned.
func present_entity(entity_presentation_snapshot: RefCounted) -> bool:
if (
entity_presentation_snapshot == null
or entity_presentation_snapshot.get_script() != ENTITY_PRESENTATION_SNAPSHOT_SCRIPT
or not bool(entity_presentation_snapshot.get("is_valid"))
):
return false
var presenter := _resolve_world_entity_presenter()
if presenter == null:
return false
return bool(presenter.call("present_entity", entity_presentation_snapshot))
## Removes one session-local entity visual without changing authoritative state.
func remove_entity(entity_id: RefCounted) -> bool:
if entity_id == null or entity_id.get_script() != ENTITY_ID_SCRIPT or not bool(entity_id.call("is_valid")):
return false
var presenter := _resolve_world_entity_presenter()
if presenter == null:
return false
return bool(presenter.call("remove_entity", entity_id))
## Returns detached entity-presentation diagnostics with no Node references.
func entity_presentation_snapshot() -> Dictionary:
var presenter := _resolve_world_entity_presenter()
if presenter == null:
return {}
var snapshot_variant = presenter.call("entity_presentation_snapshot")
if not (snapshot_variant is Dictionary):
push_error("WorldRenderFacade received a non-Dictionary entity presentation snapshot.")
return {}
return (snapshot_variant as Dictionary).duplicate(true)
func _capture_streaming_focus_from_source() -> void: func _capture_streaming_focus_from_source() -> void:
if streaming_focus_source_path == NodePath(): if streaming_focus_source_path == NodePath():
return return
@@ -177,3 +221,23 @@ func _resolve_world_environment_controller() -> Node:
_world_environment_controller = candidate _world_environment_controller = candidate
_missing_environment_controller_reported = false _missing_environment_controller_reported = false
return _world_environment_controller return _world_environment_controller
func _resolve_world_entity_presenter() -> Node:
if is_instance_valid(_world_entity_presenter):
return _world_entity_presenter
var candidate := get_node_or_null(world_entity_presenter_path)
if candidate == null:
if not _missing_entity_presenter_reported:
push_error("WorldRenderFacade cannot resolve entity presenter at %s" % world_entity_presenter_path)
_missing_entity_presenter_reported = true
return null
for required_method in [&"present_entity", &"remove_entity", &"entity_presentation_snapshot"]:
if not candidate.has_method(required_method):
if not _missing_entity_presenter_reported:
push_error("WorldRenderFacade entity presenter lacks %s" % required_method)
_missing_entity_presenter_reported = true
return null
_world_entity_presenter = candidate
_missing_entity_presenter_reported = false
return _world_entity_presenter
@@ -7,6 +7,7 @@
[ext_resource type="Script" uid="uid://bhqbo1ftrt3a3" path="res://src/scenes/character/character_appearance_presenter.gd" id="5_appearance"] [ext_resource type="Script" uid="uid://bhqbo1ftrt3a3" path="res://src/scenes/character/character_appearance_presenter.gd" id="5_appearance"]
[ext_resource type="Script" uid="uid://c2jnn0e3mmymc" path="res://src/scenes/character/character_animation_presenter.gd" id="6_animation"] [ext_resource type="Script" uid="uid://c2jnn0e3mmymc" path="res://src/scenes/character/character_animation_presenter.gd" id="6_animation"]
[ext_resource type="Script" path="res://src/render/world_render_facade.gd" id="7_render_facade"] [ext_resource type="Script" path="res://src/render/world_render_facade.gd" id="7_render_facade"]
[ext_resource type="Script" path="res://src/render/entities/world_entity_presenter.gd" id="8_entity_presenter"]
[sub_resource type="CapsuleShape3D" id="PlayerCapsuleShape_1"] [sub_resource type="CapsuleShape3D" id="PlayerCapsuleShape_1"]
radius = 0.45 radius = 0.45
@@ -99,6 +100,10 @@ script = ExtResource("7_render_facade")
streaming_world_loader_path = NodePath("..") streaming_world_loader_path = NodePath("..")
streaming_focus_source_path = NodePath("../ThirdPersonPlayer") streaming_focus_source_path = NodePath("../ThirdPersonPlayer")
world_environment_controller_path = NodePath("../WowSkyController") world_environment_controller_path = NodePath("../WowSkyController")
world_entity_presenter_path = NodePath("../WorldEntityPresenter")
[node name="WorldEntityPresenter" type="Node3D" parent="."]
script = ExtResource("8_entity_presenter")
[node name="ThirdPersonPlayer" type="CharacterBody3D" parent="." unique_id=502573687] [node name="ThirdPersonPlayer" type="CharacterBody3D" parent="." unique_id=502573687]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 16800, 80, 26400) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 16800, 80, 26400)
@@ -7,6 +7,7 @@
[ext_resource type="Script" path="res://src/scenes/character/character_appearance_presenter.gd" id="5_appearance"] [ext_resource type="Script" path="res://src/scenes/character/character_appearance_presenter.gd" id="5_appearance"]
[ext_resource type="Script" path="res://src/scenes/character/character_animation_presenter.gd" id="6_animation"] [ext_resource type="Script" path="res://src/scenes/character/character_animation_presenter.gd" id="6_animation"]
[ext_resource type="Script" path="res://src/render/world_render_facade.gd" id="7_render_facade"] [ext_resource type="Script" path="res://src/render/world_render_facade.gd" id="7_render_facade"]
[ext_resource type="Script" path="res://src/render/entities/world_entity_presenter.gd" id="8_entity_presenter"]
[sub_resource type="CapsuleShape3D" id="PlayerCapsuleShape_1"] [sub_resource type="CapsuleShape3D" id="PlayerCapsuleShape_1"]
radius = 0.45 radius = 0.45
@@ -100,6 +101,10 @@ script = ExtResource("7_render_facade")
streaming_world_loader_path = NodePath("..") streaming_world_loader_path = NodePath("..")
streaming_focus_source_path = NodePath("../ThirdPersonPlayer") streaming_focus_source_path = NodePath("../ThirdPersonPlayer")
world_environment_controller_path = NodePath("../WowSkyController") world_environment_controller_path = NodePath("../WowSkyController")
world_entity_presenter_path = NodePath("../WorldEntityPresenter")
[node name="WorldEntityPresenter" type="Node3D" parent="."]
script = ExtResource("8_entity_presenter")
[node name="ThirdPersonPlayer" type="CharacterBody3D" parent="." unique_id=502573687] [node name="ThirdPersonPlayer" type="CharacterBody3D" parent="." unique_id=502573687]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 16800, 80, 26400) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 16800, 80, 26400)
@@ -0,0 +1,10 @@
[gd_scene load_steps=2 format=3]
[sub_resource type="SphereMesh" id="SphereMesh_replacement"]
radius = 0.5
height = 1.0
[node name="ReplacementVisual" type="Node3D"]
[node name="Marker" type="MeshInstance3D" parent="."]
mesh = SubResource("SphereMesh_replacement")
@@ -0,0 +1,157 @@
extends SceneTree
## Asset-free contract, lifecycle and ownership regression for world entity visuals.
const SNAPSHOT_SCRIPT := preload("res://src/render/entities/entity_presentation_snapshot.gd")
const PRESENTER_SCRIPT := preload("res://src/render/entities/world_entity_presenter.gd")
const ENTITY_ID_SCRIPT := preload("res://src/domain/identity/entity_id.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const PRIMARY_VISUAL_PATH := "res://src/tests/scenes/character_presentation_model_fixture.tscn"
const REPLACEMENT_VISUAL_PATH := "res://src/tests/scenes/world_entity_visual_replacement_fixture.tscn"
const SNAPSHOT_SOURCE_PATH := "res://src/render/entities/entity_presentation_snapshot.gd"
const PRESENTER_SOURCE_PATH := "res://src/render/entities/world_entity_presenter.gd"
const FACADE_SOURCE_PATH := "res://src/render/world_render_facade.gd"
const RUNTIME_SCENE_PATHS: Array[String] = [
"res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
"res://src/scenes/streaming/kalimdor_streaming.tscn",
]
func _initialize() -> void:
_run_verification.call_deferred()
func _run_verification() -> void:
var failures: Array[String] = []
_verify_snapshot_contract(failures)
await _verify_presenter_lifecycle(failures)
_verify_source_boundaries(failures)
if not failures.is_empty():
for failure in failures:
push_error("WORLD_ENTITY_PRESENTATION: %s" % failure)
quit(1)
return
print("WORLD_ENTITY_PRESENTATION PASS snapshot_cases=8 lifecycle_cases=14 runtime_scenes=2")
quit(0)
func _verify_snapshot_contract(failures: Array[String]) -> void:
var entity_id: RefCounted = ENTITY_ID_SCRIPT.new(7)
var position: RefCounted = GODOT_WORLD_POSITION_SCRIPT.new(10.0, 20.0, 30.0)
var valid_snapshot := SNAPSHOT_SCRIPT.create(entity_id, position, PRIMARY_VISUAL_PATH, 0.5, 2.0, false)
_expect_true(bool(valid_snapshot.get("is_valid")), "valid snapshot", failures)
_expect_true(int(SNAPSHOT_SCRIPT.CONTRACT_VERSION) == 1, "contract version", failures)
_expect_failure(SNAPSHOT_SCRIPT.create(ENTITY_ID_SCRIPT.new(0), position, PRIMARY_VISUAL_PATH), &"entity_presentation_invalid_entity_id", "invalid entity", failures)
_expect_failure(SNAPSHOT_SCRIPT.create(entity_id, RefCounted.new(), PRIMARY_VISUAL_PATH), &"entity_presentation_invalid_world_position", "invalid position type", failures)
_expect_failure(SNAPSHOT_SCRIPT.create(entity_id, GODOT_WORLD_POSITION_SCRIPT.new(INF, 0.0, 0.0), PRIMARY_VISUAL_PATH), &"entity_presentation_position_not_finite", "non-finite position", failures)
_expect_failure(SNAPSHOT_SCRIPT.create(entity_id, position, ""), &"entity_presentation_visual_path_empty", "empty visual path", failures)
_expect_failure(SNAPSHOT_SCRIPT.create(entity_id, position, PRIMARY_VISUAL_PATH, NAN), &"entity_presentation_yaw_not_finite", "non-finite yaw", failures)
_expect_failure(SNAPSHOT_SCRIPT.create(entity_id, position, PRIMARY_VISUAL_PATH, 0.0, 0.0), &"entity_presentation_scale_invalid", "zero scale", failures)
func _verify_presenter_lifecycle(failures: Array[String]) -> void:
var presenter: Node3D = PRESENTER_SCRIPT.new()
presenter.name = "WorldEntityPresenter"
root.add_child(presenter)
var entity_id: RefCounted = ENTITY_ID_SCRIPT.new(7)
var initial_snapshot := SNAPSHOT_SCRIPT.create(
entity_id,
GODOT_WORLD_POSITION_SCRIPT.new(10.0, 20.0, 30.0),
PRIMARY_VISUAL_PATH,
0.5,
2.0,
false
)
_expect_true(bool(presenter.call("present_entity", initial_snapshot)), "initial present", failures)
var visual_root := presenter.get_node_or_null("EntityVisual_00000000000000000007") as Node3D
_expect_true(visual_root != null, "owned visual attached", failures)
if visual_root != null:
_expect_vector_near(visual_root.global_position, Vector3(10.0, 20.0, 30.0), "initial position", failures)
_expect_near(visual_root.scale.x, 2.0, "initial scale", failures)
_expect_true(not visual_root.visible, "initial visibility", failures)
var original_instance_id := visual_root.get_instance_id() if visual_root != null else 0
var updated_snapshot := SNAPSHOT_SCRIPT.create(
entity_id,
GODOT_WORLD_POSITION_SCRIPT.new(40.0, 50.0, 60.0),
PRIMARY_VISUAL_PATH,
1.0,
1.5,
true
)
_expect_true(bool(presenter.call("present_entity", updated_snapshot)), "idempotent update", failures)
visual_root = presenter.get_node_or_null("EntityVisual_00000000000000000007") as Node3D
_expect_true(visual_root != null and visual_root.get_instance_id() == original_instance_id, "same visual root retained", failures)
if visual_root != null:
_expect_vector_near(visual_root.global_position, Vector3(40.0, 50.0, 60.0), "updated position", failures)
_expect_true(visual_root.visible, "updated visibility", failures)
var failed_replacement := SNAPSHOT_SCRIPT.create(entity_id, GODOT_WORLD_POSITION_SCRIPT.new(1.0, 2.0, 3.0), "res://missing/entity_visual.tscn")
_expect_true(not bool(presenter.call("present_entity", failed_replacement)), "missing replacement rejected", failures)
visual_root = presenter.get_node_or_null("EntityVisual_00000000000000000007") as Node3D
_expect_true(visual_root != null and visual_root.get_instance_id() == original_instance_id, "failed replacement retains visual", failures)
var replacement_snapshot := SNAPSHOT_SCRIPT.create(entity_id, GODOT_WORLD_POSITION_SCRIPT.new(70.0, 80.0, 90.0), REPLACEMENT_VISUAL_PATH)
_expect_true(bool(presenter.call("present_entity", replacement_snapshot)), "valid replacement", failures)
visual_root = presenter.get_node_or_null("EntityVisual_00000000000000000007") as Node3D
_expect_true(visual_root != null and visual_root.get_instance_id() != original_instance_id, "replacement swaps root", failures)
var diagnostic_snapshot: Dictionary = presenter.call("entity_presentation_snapshot")
_expect_true(int(diagnostic_snapshot.get("contract_version", 0)) == 1, "diagnostic version", failures)
_expect_true(int(diagnostic_snapshot.get("entity_count", 0)) == 1, "diagnostic entity count", failures)
(diagnostic_snapshot.get("entities") as Array).clear()
_expect_true(int((presenter.call("entity_presentation_snapshot") as Dictionary).get("entity_count", 0)) == 1, "diagnostic detached", failures)
_expect_true(bool(presenter.call("remove_entity", entity_id)), "known entity removed", failures)
_expect_true(not bool(presenter.call("remove_entity", entity_id)), "duplicate removal rejected", failures)
_expect_true(presenter.get_child_count() == 0, "owned visual detached", failures)
presenter.free()
await process_frame
func _verify_source_boundaries(failures: Array[String]) -> void:
var snapshot_source := _read_text(SNAPSHOT_SOURCE_PATH, failures)
for forbidden_snapshot_text in ["extends Node", "Node3D", "PackedScene", "Dictionary", "Vector3"]:
_expect_true(not snapshot_source.contains(forbidden_snapshot_text), "snapshot omits %s" % forbidden_snapshot_text, failures)
var presenter_source := _read_text(PRESENTER_SOURCE_PATH, failures)
for forbidden_presenter_text in ["MoveIntent", "TerrainQuery", "WowGuid", "network", "server_data"]:
_expect_true(not presenter_source.contains(forbidden_presenter_text), "presenter omits %s" % forbidden_presenter_text, failures)
for required_presenter_text in ["ResourceLoader.exists", "remove_child(visual_root)", "visual_root.queue_free()"]:
_expect_true(presenter_source.contains(required_presenter_text), "presenter contains %s" % required_presenter_text, failures)
var facade_source := _read_text(FACADE_SOURCE_PATH, failures)
for required_facade_method in ["func present_entity(", "func remove_entity(", "func entity_presentation_snapshot()"]:
_expect_true(facade_source.contains(required_facade_method), "facade contains %s" % required_facade_method, failures)
for scene_path in RUNTIME_SCENE_PATHS:
var scene_source := _read_text(scene_path, failures)
_expect_true(scene_source.contains("world_entity_presenter.gd"), "%s loads presenter" % scene_path, failures)
_expect_true(scene_source.contains('world_entity_presenter_path = NodePath("../WorldEntityPresenter")'), "%s facade path" % scene_path, failures)
func _read_text(path: String, failures: Array[String]) -> String:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("cannot open %s" % path)
return ""
return file.get_as_text()
func _expect_failure(snapshot: RefCounted, expected_code: StringName, label: String, failures: Array[String]) -> void:
_expect_true(not bool(snapshot.get("is_valid")), "%s invalid" % label, failures)
_expect_true(StringName(snapshot.get("failure_code")) == expected_code, "%s failure code" % label, failures)
func _expect_vector_near(actual_value: Vector3, expected_value: Vector3, label: String, failures: Array[String]) -> void:
if not actual_value.is_equal_approx(expected_value):
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
func _expect_near(actual_value: float, expected_value: float, label: String, failures: Array[String]) -> void:
if not is_equal_approx(actual_value, expected_value):
failures.append("%s expected %.3f, got %.3f" % [label, expected_value, actual_value])
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
if not actual_value:
failures.append("%s expected true" % label)
@@ -0,0 +1 @@
uid://dkkjbc8pcglcp
+41 -1
View File
@@ -8,6 +8,8 @@ const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_fo
const RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd") const RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
const WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT := preload("res://src/render/environment/world_environment_snapshot.gd") const WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT := preload("res://src/render/environment/world_environment_snapshot.gd")
const WOW_SKY_CONTROLLER_SCRIPT := preload("res://src/scenes/sky/wow_sky_controller.gd") const WOW_SKY_CONTROLLER_SCRIPT := preload("res://src/scenes/sky/wow_sky_controller.gd")
const ENTITY_PRESENTATION_SNAPSHOT_SCRIPT := preload("res://src/render/entities/entity_presentation_snapshot.gd")
const ENTITY_ID_SCRIPT := preload("res://src/domain/identity/entity_id.gd")
const RUNTIME_SCENE_PATHS: Array[String] = [ const RUNTIME_SCENE_PATHS: Array[String] = [
"res://src/scenes/streaming/eastern_kingdoms_streaming.tscn", "res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
@@ -57,6 +59,23 @@ class WorldEnvironmentControllerDouble extends Node:
return true return true
class WorldEntityPresenterDouble extends Node:
var presented_snapshot: RefCounted
var removed_entity_id: RefCounted
var diagnostics := {"entity_count": 1, "entities": [{"entity_key": "entity:7"}]}
func present_entity(entity_presentation_snapshot: RefCounted) -> bool:
presented_snapshot = entity_presentation_snapshot
return true
func remove_entity(entity_id: RefCounted) -> bool:
removed_entity_id = entity_id
return true
func entity_presentation_snapshot() -> Dictionary:
return diagnostics
func _initialize() -> void: func _initialize() -> void:
var failures: Array[String] = [] var failures: Array[String] = []
_verify_rendered_ground_sample_contract(failures) _verify_rendered_ground_sample_contract(failures)
@@ -70,7 +89,7 @@ func _initialize() -> void:
quit(1) quit(1)
return return
print("WORLD_RENDER_FACADE PASS delegation=6 ground_contract=2 environment_contract=7 runtime_scenes=2 tools=3") print("WORLD_RENDER_FACADE PASS delegation=9 ground_contract=2 environment_contract=7 entity_contract=6 runtime_scenes=2 tools=3")
quit(0) quit(0)
@@ -116,14 +135,18 @@ func _verify_delegation_and_snapshot_isolation(failures: Array[String]) -> void:
source.position = Vector3(10.0, 20.0, 30.0) source.position = Vector3(10.0, 20.0, 30.0)
var environment_controller := WorldEnvironmentControllerDouble.new() var environment_controller := WorldEnvironmentControllerDouble.new()
environment_controller.name = "WorldEnvironmentControllerDouble" environment_controller.name = "WorldEnvironmentControllerDouble"
var entity_presenter := WorldEntityPresenterDouble.new()
entity_presenter.name = "WorldEntityPresenterDouble"
var facade = WORLD_RENDER_FACADE_SCRIPT.new() var facade = WORLD_RENDER_FACADE_SCRIPT.new()
facade.name = "WorldRenderFacade" facade.name = "WorldRenderFacade"
facade.streaming_world_loader_path = NodePath("../StreamingWorldLoaderDouble") facade.streaming_world_loader_path = NodePath("../StreamingWorldLoaderDouble")
facade.streaming_focus_source_path = NodePath("../FocusSource") facade.streaming_focus_source_path = NodePath("../FocusSource")
facade.world_environment_controller_path = NodePath("../WorldEnvironmentControllerDouble") facade.world_environment_controller_path = NodePath("../WorldEnvironmentControllerDouble")
facade.world_entity_presenter_path = NodePath("../WorldEntityPresenterDouble")
test_root.add_child(loader) test_root.add_child(loader)
test_root.add_child(source) test_root.add_child(source)
test_root.add_child(environment_controller) test_root.add_child(environment_controller)
test_root.add_child(entity_presenter)
test_root.add_child(facade) test_root.add_child(facade)
get_root().add_child(test_root) get_root().add_child(test_root)
await process_frame await process_frame
@@ -181,6 +204,21 @@ func _verify_delegation_and_snapshot_isolation(failures: Array[String]) -> void:
"invalid environment snapshot rejected", "invalid environment snapshot rejected",
failures failures
) )
var entity_id: RefCounted = ENTITY_ID_SCRIPT.new(7)
var entity_snapshot: RefCounted = ENTITY_PRESENTATION_SNAPSHOT_SCRIPT.create(
entity_id,
GODOT_WORLD_POSITION_SCRIPT.new(1.0, 2.0, 3.0),
"res://src/tests/scenes/character_presentation_model_fixture.tscn"
)
_expect_true(facade.present_entity(entity_snapshot), "entity presentation delegates", failures)
_expect_true(entity_presenter.presented_snapshot == entity_snapshot, "entity snapshot reference delegates", failures)
_expect_true(facade.remove_entity(entity_id), "entity removal delegates", failures)
_expect_true(entity_presenter.removed_entity_id == entity_id, "entity identity reference delegates", failures)
var entity_diagnostics: Dictionary = facade.entity_presentation_snapshot()
(entity_diagnostics.get("entities") as Array).clear()
_expect_true(entity_presenter.diagnostics.entities.size() == 1, "entity diagnostics detached", failures)
_expect_true(not facade.present_entity(ENTITY_PRESENTATION_SNAPSHOT_SCRIPT.create(ENTITY_ID_SCRIPT.new(0), GODOT_WORLD_POSITION_SCRIPT.new(0.0, 0.0, 0.0), "res://fixture.tscn")), "invalid entity snapshot rejected", failures)
test_root.queue_free() test_root.queue_free()
@@ -191,6 +229,8 @@ func _verify_repository_wiring(failures: Array[String]) -> void:
_expect_true(scene_source.contains('[node name="WorldRenderFacade" type="Node" parent="."]'), "%s owns facade node" % scene_path, failures) _expect_true(scene_source.contains('[node name="WorldRenderFacade" type="Node" parent="."]'), "%s owns facade node" % scene_path, failures)
_expect_true(scene_source.contains('streaming_focus_source_path = NodePath("../ThirdPersonPlayer")'), "%s facade uses player focus" % scene_path, failures) _expect_true(scene_source.contains('streaming_focus_source_path = NodePath("../ThirdPersonPlayer")'), "%s facade uses player focus" % scene_path, failures)
_expect_true(scene_source.contains('world_environment_controller_path = NodePath("../WowSkyController")'), "%s facade uses sky controller" % scene_path, failures) _expect_true(scene_source.contains('world_environment_controller_path = NodePath("../WowSkyController")'), "%s facade uses sky controller" % scene_path, failures)
_expect_true(scene_source.contains('world_entity_presenter_path = NodePath("../WorldEntityPresenter")'), "%s facade uses entity presenter" % scene_path, failures)
_expect_true(scene_source.contains('[node name="WorldEntityPresenter" type="Node3D" parent="."]'), "%s owns entity presenter service" % scene_path, failures)
for tool_path in TOOL_PATHS: for tool_path in TOOL_PATHS:
var tool_source := _read_text(tool_path, failures) var tool_source := _read_text(tool_path, failures)
+13 -5
View File
@@ -8,7 +8,7 @@
## Steps ## Steps
- [ ] Ввести `WorldRenderFacade` для focus, environment, entity visuals, ground query и metrics. - [x] Ввести `WorldRenderFacade` для focus, environment, entity visuals, ground query и metrics.
- [x] Запретить gameplay/editor доступ к внутренним очередям streamer. - [x] Запретить gameplay/editor доступ к внутренним очередям streamer.
- [x] Извлечь pure `StreamingTargetPlanner`. - [x] Извлечь pure `StreamingTargetPlanner`.
- [x] Извлечь `RenderBudgetScheduler` с bounded queues/cancellation. - [x] Извлечь `RenderBudgetScheduler` с bounded queues/cancellation.
@@ -27,9 +27,10 @@ Runtime и Editor используют facade; planner/scheduler тестиру
## Evidence ## Evidence
- Date: 2026-07-16 - Date: 2026-07-16
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`; - Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`;
packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`, packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`,
`M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001` `M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`,
`M03-RND-FACADE-ENTITY-001`
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown - Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates. headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner - Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
@@ -41,7 +42,10 @@ Runtime и Editor используют facade; planner/scheduler тестиру
`delegation=5 ground_contract=2`; cold `RenderedGroundSample contract=3`; `delegation=5 ground_contract=2`; cold `RenderedGroundSample contract=3`;
environment integration passed cold snapshot `contract=5` and facade environment integration passed cold snapshot `contract=5` and facade
`delegation=6 environment_contract=7`; dry-run retained all `7/7` plans and `delegation=6 environment_contract=7`; dry-run retained all `7/7` plans and
applied the dusk checkpoint at `time=19.00`. M02 terrain-query regression applied the dusk checkpoint at `time=19.00`. Entity presentation passed
`snapshot_cases=8 lifecycle_cases=14 runtime_scenes=2`; facade expanded to
`delegation=9 entity_contract=6`, while character presentation remained `9/10/3/1`.
M02 terrain-query regression
remained green (13 pre-existing expired M00 claim warnings). remained green (13 pre-existing expired M00 claim warnings).
- Fidelity comparison: all 16 historical operation limits and drain sites were - Fidelity comparison: all 16 historical operation limits and drain sites were
preserved; chunk removal/create retain their shared removal-first budget. preserved; chunk removal/create retain their shared removal-first budget.
@@ -50,14 +54,18 @@ Runtime и Editor используют facade; planner/scheduler тестиру
`2/5/10/20/40`-unit nearby fallback while hiding mutable renderer state. `2/5/10/20/40`-unit nearby fallback while hiding mutable renderer state.
Environment snapshots preserve the prior fixed-clock assignments while DBC Environment snapshots preserve the prior fixed-clock assignments while DBC
profile/area/skybox selection, interpolation, fog, sun and shader rules remain unchanged. profile/area/skybox selection, interpolation, fog, sun and shader rules remain unchanged.
Runtime produces no world-entity commands yet, so player/world output and all
baseline plans remain unchanged by the new present/remove ownership boundary.
No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending. No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending.
- Changed files: scene-free scheduler and verifier; streamer budget composition/ - Changed files: scene-free scheduler and verifier; streamer budget composition/
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes; permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
renderer-owned ground sample, facade/loader ground query, migrated terrain probe, renderer-owned ground sample, facade/loader ground query, migrated terrain probe,
immutable environment snapshot, facade/sky adapter, migrated checkpoint time input, immutable environment snapshot, facade/sky adapter, migrated checkpoint time input,
contract-v1 entity snapshot, renderer-owned entity presenter, runtime composition,
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence. expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
- Remaining risks: queue storage length and worker concurrency/stale-result logic - Remaining risks: queue storage length and worker concurrency/stale-result logic
remain streamer-owned; cancellation stops new permits but does not interrupt remain streamer-owned; cancellation stops new permits but does not interrupt
in-flight work; the environment snapshot currently covers world time but not weather in-flight work; the environment snapshot currently covers world time but not weather
or indoor state; M03 still needs entity facade commands, service extraction or indoor state; entity PackedScene loading is synchronous and not accepted for
network-scale hot paths; M03 still needs service extraction
and dependency/performance acceptance. and dependency/performance acceptance.