Compare commits
6 Commits
a85e416cb8
...
61bad09a9c
| Author | SHA1 | Date | |
|---|---|---|---|
| 61bad09a9c | |||
| 606770c525 | |||
| b73b2e74a2 | |||
| 1807c3a363 | |||
| 3c5880f190 | |||
| a7b2f9475b |
@@ -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
|
||||
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
|
||||
|
||||
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.
|
||||
@@ -15,6 +15,7 @@
|
||||
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.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) |
|
||||
| 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) |
|
||||
| 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) |
|
||||
|
||||
@@ -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)
|
||||
@@ -5,9 +5,9 @@
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
## Purpose
|
||||
@@ -49,6 +49,10 @@ flowchart LR
|
||||
Facade --> SkyController[WowSkyController]
|
||||
Sky[DBC sky/light data] --> SkyController
|
||||
SkyController --> Scene
|
||||
EntityState[Entity visual read model] --> EntitySnapshot[EntityPresentationSnapshot]
|
||||
EntitySnapshot --> Facade
|
||||
Facade --> EntityPresenter[WorldEntityPresenter]
|
||||
EntityPresenter --> Scene
|
||||
```
|
||||
|
||||
Allowed dependencies:
|
||||
@@ -69,9 +73,9 @@ Forbidden dependencies:
|
||||
## Public API
|
||||
|
||||
Runtime, capture and probe callers use `WorldRenderFacade` for focus, immutable
|
||||
environment input, metrics and rendered-ground diagnostics. `StreamingWorldLoader`
|
||||
and `WowSkyController` remain internal scene implementations while a later M03
|
||||
package adds entity-visual commands.
|
||||
environment/entity presentation input, metrics and rendered-ground diagnostics.
|
||||
`StreamingWorldLoader`, `WowSkyController` and `WorldEntityPresenter` remain
|
||||
internal scene implementations.
|
||||
Gameplay modules and Godot `EditorPlugin` package sources are repository-gated
|
||||
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.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 |
|
||||
| `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 |
|
||||
| `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 |
|
||||
@@ -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 | 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 | `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 | 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 |
|
||||
@@ -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 | `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 | 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 |
|
||||
| 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]
|
||||
DBC[Light/Area/Skybox DBC] --> SkyController
|
||||
SkyController --> World
|
||||
EntityReadModel[Entity visual read model] --> EntitySnapshot[EntityPresentationSnapshot]
|
||||
EntitySnapshot --> RF
|
||||
RF --> EntityPresenter[WorldEntityPresenter]
|
||||
EntityAsset[PackedScene path] --> EntityPresenter
|
||||
EntityPresenter --> World
|
||||
Terrain --> World[Godot world]
|
||||
M2 --> World
|
||||
WMO --> World
|
||||
@@ -218,6 +236,11 @@ sequenceDiagram
|
||||
Source->>Facade: apply_environment_snapshot(exact time)
|
||||
Facade->>Render: delegate to WowSkyController
|
||||
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->>Worker: shutdown: wait for WorkerThreadPool tasks
|
||||
Stream->>Stream: shutdown: finish registered ResourceLoader requests
|
||||
@@ -236,6 +259,10 @@ sequenceDiagram
|
||||
into its existing fixed-clock fields on the main thread.
|
||||
- `WowSkyController` continues to own DBC sampling state and mutations of its
|
||||
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;
|
||||
the facade never returns Mesh, Node, tile-state or queue references.
|
||||
- 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 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -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/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/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
|
||||
| `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_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_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_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 |
|
||||
|
||||
@@ -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
|
||||
@@ -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 RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
|
||||
const WORLD_ENVIRONMENT_SNAPSHOT_SCRIPT := preload("res://src/render/environment/world_environment_snapshot.gd")
|
||||
const ENTITY_PRESENTATION_SNAPSHOT_SCRIPT := preload("res://src/render/entities/entity_presentation_snapshot.gd")
|
||||
const ENTITY_ID_SCRIPT := preload("res://src/domain/identity/entity_id.gd")
|
||||
|
||||
## Internal renderer implementation resolved relative to this node.
|
||||
## The referenced node remains owned by its scene parent.
|
||||
@@ -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.
|
||||
@export var world_environment_controller_path: NodePath = NodePath("../WowSkyController")
|
||||
|
||||
## Scene-owned service that creates and owns session-local world entity visuals.
|
||||
@export var world_entity_presenter_path: NodePath = NodePath("../WorldEntityPresenter")
|
||||
|
||||
## Optional explicit Node3D source sampled in Godot world coordinates.
|
||||
## Runtime scenes use the player; capture and probe tools replace it with their camera.
|
||||
@export var streaming_focus_source_path: NodePath
|
||||
|
||||
var _streaming_world_loader: Node
|
||||
var _world_environment_controller: Node
|
||||
var _world_entity_presenter: Node
|
||||
var _missing_loader_reported := false
|
||||
var _missing_environment_controller_reported := false
|
||||
var _missing_entity_presenter_reported := false
|
||||
var _missing_focus_source_reported := false
|
||||
|
||||
|
||||
@@ -116,6 +123,43 @@ func apply_environment_snapshot(world_environment_snapshot: RefCounted) -> bool:
|
||||
return bool(controller.call("apply_environment_snapshot", world_environment_snapshot))
|
||||
|
||||
|
||||
## Idempotently creates or updates one world entity visual through the renderer-
|
||||
## owned presenter. The immutable full snapshot remains caller-owned.
|
||||
func present_entity(entity_presentation_snapshot: RefCounted) -> bool:
|
||||
if (
|
||||
entity_presentation_snapshot == null
|
||||
or entity_presentation_snapshot.get_script() != ENTITY_PRESENTATION_SNAPSHOT_SCRIPT
|
||||
or not bool(entity_presentation_snapshot.get("is_valid"))
|
||||
):
|
||||
return false
|
||||
var presenter := _resolve_world_entity_presenter()
|
||||
if presenter == null:
|
||||
return false
|
||||
return bool(presenter.call("present_entity", entity_presentation_snapshot))
|
||||
|
||||
|
||||
## Removes one session-local entity visual without changing authoritative state.
|
||||
func remove_entity(entity_id: RefCounted) -> bool:
|
||||
if entity_id == null or entity_id.get_script() != ENTITY_ID_SCRIPT or not bool(entity_id.call("is_valid")):
|
||||
return false
|
||||
var presenter := _resolve_world_entity_presenter()
|
||||
if presenter == null:
|
||||
return false
|
||||
return bool(presenter.call("remove_entity", entity_id))
|
||||
|
||||
|
||||
## Returns detached entity-presentation diagnostics with no Node references.
|
||||
func entity_presentation_snapshot() -> Dictionary:
|
||||
var presenter := _resolve_world_entity_presenter()
|
||||
if presenter == null:
|
||||
return {}
|
||||
var snapshot_variant = presenter.call("entity_presentation_snapshot")
|
||||
if not (snapshot_variant is Dictionary):
|
||||
push_error("WorldRenderFacade received a non-Dictionary entity presentation snapshot.")
|
||||
return {}
|
||||
return (snapshot_variant as Dictionary).duplicate(true)
|
||||
|
||||
|
||||
func _capture_streaming_focus_from_source() -> void:
|
||||
if streaming_focus_source_path == NodePath():
|
||||
return
|
||||
@@ -177,3 +221,23 @@ func _resolve_world_environment_controller() -> Node:
|
||||
_world_environment_controller = candidate
|
||||
_missing_environment_controller_reported = false
|
||||
return _world_environment_controller
|
||||
|
||||
|
||||
func _resolve_world_entity_presenter() -> Node:
|
||||
if is_instance_valid(_world_entity_presenter):
|
||||
return _world_entity_presenter
|
||||
var candidate := get_node_or_null(world_entity_presenter_path)
|
||||
if candidate == null:
|
||||
if not _missing_entity_presenter_reported:
|
||||
push_error("WorldRenderFacade cannot resolve entity presenter at %s" % world_entity_presenter_path)
|
||||
_missing_entity_presenter_reported = true
|
||||
return null
|
||||
for required_method in [&"present_entity", &"remove_entity", &"entity_presentation_snapshot"]:
|
||||
if not candidate.has_method(required_method):
|
||||
if not _missing_entity_presenter_reported:
|
||||
push_error("WorldRenderFacade entity presenter lacks %s" % required_method)
|
||||
_missing_entity_presenter_reported = true
|
||||
return null
|
||||
_world_entity_presenter = candidate
|
||||
_missing_entity_presenter_reported = false
|
||||
return _world_entity_presenter
|
||||
|
||||
@@ -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://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/entities/world_entity_presenter.gd" id="8_entity_presenter"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="PlayerCapsuleShape_1"]
|
||||
radius = 0.45
|
||||
@@ -99,6 +100,10 @@ script = ExtResource("7_render_facade")
|
||||
streaming_world_loader_path = NodePath("..")
|
||||
streaming_focus_source_path = NodePath("../ThirdPersonPlayer")
|
||||
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]
|
||||
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_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/entities/world_entity_presenter.gd" id="8_entity_presenter"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="PlayerCapsuleShape_1"]
|
||||
radius = 0.45
|
||||
@@ -100,6 +101,10 @@ script = ExtResource("7_render_facade")
|
||||
streaming_world_loader_path = NodePath("..")
|
||||
streaming_focus_source_path = NodePath("../ThirdPersonPlayer")
|
||||
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]
|
||||
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
|
||||
@@ -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 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 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] = [
|
||||
"res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
|
||||
@@ -57,6 +59,23 @@ class WorldEnvironmentControllerDouble extends Node:
|
||||
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:
|
||||
var failures: Array[String] = []
|
||||
_verify_rendered_ground_sample_contract(failures)
|
||||
@@ -70,7 +89,7 @@ func _initialize() -> void:
|
||||
quit(1)
|
||||
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)
|
||||
|
||||
|
||||
@@ -116,14 +135,18 @@ func _verify_delegation_and_snapshot_isolation(failures: Array[String]) -> void:
|
||||
source.position = Vector3(10.0, 20.0, 30.0)
|
||||
var environment_controller := WorldEnvironmentControllerDouble.new()
|
||||
environment_controller.name = "WorldEnvironmentControllerDouble"
|
||||
var entity_presenter := WorldEntityPresenterDouble.new()
|
||||
entity_presenter.name = "WorldEntityPresenterDouble"
|
||||
var facade = WORLD_RENDER_FACADE_SCRIPT.new()
|
||||
facade.name = "WorldRenderFacade"
|
||||
facade.streaming_world_loader_path = NodePath("../StreamingWorldLoaderDouble")
|
||||
facade.streaming_focus_source_path = NodePath("../FocusSource")
|
||||
facade.world_environment_controller_path = NodePath("../WorldEnvironmentControllerDouble")
|
||||
facade.world_entity_presenter_path = NodePath("../WorldEntityPresenterDouble")
|
||||
test_root.add_child(loader)
|
||||
test_root.add_child(source)
|
||||
test_root.add_child(environment_controller)
|
||||
test_root.add_child(entity_presenter)
|
||||
test_root.add_child(facade)
|
||||
get_root().add_child(test_root)
|
||||
await process_frame
|
||||
@@ -181,6 +204,21 @@ func _verify_delegation_and_snapshot_isolation(failures: Array[String]) -> void:
|
||||
"invalid environment snapshot rejected",
|
||||
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()
|
||||
|
||||
|
||||
@@ -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('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_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:
|
||||
var tool_source := _read_text(tool_path, failures)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## Steps
|
||||
|
||||
- [ ] Ввести `WorldRenderFacade` для focus, environment, entity visuals, ground query и metrics.
|
||||
- [x] Ввести `WorldRenderFacade` для focus, environment, entity visuals, ground query и metrics.
|
||||
- [x] Запретить gameplay/editor доступ к внутренним очередям streamer.
|
||||
- [x] Извлечь pure `StreamingTargetPlanner`.
|
||||
- [x] Извлечь `RenderBudgetScheduler` с bounded queues/cancellation.
|
||||
@@ -27,9 +27,10 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
## Evidence
|
||||
|
||||
- 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`,
|
||||
`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
|
||||
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
|
||||
- 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`;
|
||||
environment integration passed cold snapshot `contract=5` and facade
|
||||
`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).
|
||||
- Fidelity comparison: all 16 historical operation limits and drain sites were
|
||||
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.
|
||||
Environment snapshots preserve the prior fixed-clock assignments while DBC
|
||||
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.
|
||||
- Changed files: scene-free scheduler and verifier; streamer budget composition/
|
||||
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
|
||||
renderer-owned ground sample, facade/loader ground query, migrated terrain probe,
|
||||
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.
|
||||
- Remaining risks: queue storage length and worker concurrency/stale-result logic
|
||||
remain streamer-owned; cancellation stops new permits but does not interrupt
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user