Compare commits

...

34 Commits

Author SHA1 Message Date
sindoring 53d84ccc8a docs(M03): accept streamer internal access gate 2026-07-16 00:38:58 +04:00
sindoring 7e35de7f61 merge(M03): integrate streamer internal access gate 2026-07-16 00:37:29 +04:00
sindoring ba9f89691d coord(M03): hand off streamer access gate 2026-07-16 00:37:15 +04:00
sindoring b58bf2caa6 test(M03): enforce streamer internal access boundary 2026-07-16 00:37:00 +04:00
sindoring 52ea639d64 coord(M03): claim streamer internal access gate 2026-07-16 00:33:32 +04:00
sindoring a023d76eea docs(M03): accept render budget scheduler integration 2026-07-16 00:32:11 +04:00
sindoring 7ece2abea2 merge(M03): integrate render budget scheduler 2026-07-16 00:31:30 +04:00
sindoring 10c5d4cc12 coord(M03): hand off render budget scheduler 2026-07-16 00:31:21 +04:00
sindoring e52f703da5 rnd(M03): extract render budget scheduler 2026-07-16 00:31:01 +04:00
sindoring 91f0724ce2 coord(M03): claim render budget scheduler 2026-07-16 00:22:16 +04:00
sindoring 650d584ff0 docs(M03): accept streaming planner integration
Work-Package: M03-RND-STREAMING-PLANNER-001
Agent: sindo-main-codex
Tests: post-merge planner/facade/focus/coordinate/manifest/shutdown and repository gates pass
Fidelity: target selection formulas and renderer behavior preserved
2026-07-16 00:11:57 +04:00
sindoring 643850c8ee Merge planner cold-start fix
Work-Package: M03-RND-STREAMING-PLANNER-001
Agent: sindo-main-codex
Tests: stale-class-cache headless verifier passes
Fidelity: no planning behavior change
2026-07-16 00:11:20 +04:00
sindoring ad8fc41ace fix(M03): support cold planner startup
Work-Package: M03-RND-STREAMING-PLANNER-001
Agent: sindo-main-codex
Tests: stale-class-cache headless planner verifier passes
Fidelity: type-resolution-only fix; planning behavior unchanged
2026-07-16 00:11:07 +04:00
sindoring d0f74f840b Merge work package M03-RND-STREAMING-PLANNER-001
Work-Package: M03-RND-STREAMING-PLANNER-001
Agent: sindo-main-codex
Tests: planner/facade/focus/coordinate/manifest/shutdown contracts, dry-run and repository gates
Fidelity: preserves target sets, boundary prefetch, queue priority, caches and visible renderer behavior
2026-07-16 00:08:32 +04:00
sindoring 5b10288ff7 docs(M03): publish streaming planner handoff
Work-Package: M03-RND-STREAMING-PLANNER-001
Agent: sindo-main-codex
Tests: planner and repository gates pass
Fidelity: no renderer policy or visual change
2026-07-16 00:08:24 +04:00
sindoring e887c3bad5 rnd(M03): extract streaming target planner
Work-Package: M03-RND-STREAMING-PLANNER-001
Agent: sindo-main-codex
Tests: planner/facade/focus/coordinate/manifest/shutdown contracts, seven-checkpoint dry-run, repository gates
Fidelity: preserves wanted/retained radii, boundary prefetch, editor preview, queue priority and cache/render behavior
2026-07-16 00:08:02 +04:00
sindoring 0c24f96ae1 rnd(M03): claim streaming target planner
Work-Package: M03-RND-STREAMING-PLANNER-001
Agent: sindo-main-codex
Tests: not run (coordination-only commit)
Fidelity: no runtime change
2026-07-15 23:58:57 +04:00
sindoring ad0532925a qar(M03): exclude reference projects from Godot imports
Work-Package: M03-QAR-REFERENCE-IMPORTS-001
Agent: sindo-main-codex
Tests: headless Godot rescan; nested reference repositories remain clean
Fidelity: reference-only tooling change; runtime and renderer behavior unchanged
2026-07-15 23:56:15 +04:00
sindoring 9c3f7018fb docs(M03): accept renderer facade integration
Work-Package: M03-RND-FACADE-001
Agent: sindo-main-codex
Tests: post-merge verification follows
Fidelity: no runtime change
2026-07-15 23:49:59 +04:00
sindoring a100cede46 Merge work package M03-RND-FACADE-001
Work-Package: M03-RND-FACADE-001
Agent: sindo-main-codex
Tests: facade/focus/coordinate/manifest contracts, dry-run, coordination and documentation gates
Fidelity: no renderer target, budget, cache or visual behavior change
2026-07-15 23:49:49 +04:00
sindoring 2466fd67d3 docs(M03): publish renderer facade handoff
Work-Package: M03-RND-FACADE-001
Agent: sindo-main-codex
Tests: facade/focus contracts and repository gates pass
Fidelity: no visual or streaming-policy change
2026-07-15 23:49:21 +04:00
sindoring 3d528e3bbf rnd(M03): introduce renderer facade seam
Work-Package: M03-RND-FACADE-001
Agent: sindo-main-codex
Tests: facade/focus/coordinate/manifest contracts, renderer dry-run, coordination and documentation gates
Fidelity: focus targets, budgets, queues, caches and visible renderer behavior remain unchanged
2026-07-15 23:48:53 +04:00
sindoring a629bedccf rnd(M03): claim renderer facade seam
Work-Package: M03-RND-FACADE-001
Agent: sindo-main-codex
Tests: not run (coordination-only commit)
Fidelity: no runtime change
2026-07-15 23:43:50 +04:00
sindoring 4f566b1957 Merge pull request 'qar(M02): close player decomposition milestone' (#28) from work/sindo-main-codex/m02-integrator-closeout into master
Reviewed-on: #28
2026-07-15 22:38:42 +03:00
sindoring d9c847c557 docs(M02): hand off milestone closeout
Work-Package: M02-QAR-INTEGRATOR-CLOSEOUT-001
Agent: sindo-main-codex
Tests: records complete post-merge and renderer verification
Fidelity: records direct, reference-only and open-gap classifications
2026-07-15 23:35:22 +04:00
sindoring 1bf5b526be qar(M02): close player decomposition milestone
Work-Package: M02-QAR-INTEGRATOR-CLOSEOUT-001
Agent: sindo-main-codex
Tests: all M02 contracts, renderer dry-run, documentation and coordination gates passed
Fidelity: direct binding evidence and reference-only timing/routing are separated; remaining locomotion/camera gaps stay explicit
2026-07-15 23:35:00 +04:00
sindoring 9275fbe04f reference, scenes 2026-07-15 23:32:50 +04:00
sindoring eb122d52a9 Merge pull request 'gmp(M02): add 3.3.5 input profile' (#27) from work/sindo-main-codex/m02-input-profile-335 into master
Reviewed-on: #27
2026-07-14 23:20:22 +03:00
sindoring e819eeb35b docs(M02): hand off 3.3.5 input profile
Work-Package: M02-GMP-INPUT-PROFILE-335-001
Agent: sindo-main-codex
Tests: records complete package and renderer verification
Fidelity: records private-source hashes, public reference revisions and remaining gaps
2026-07-15 00:18:03 +04:00
sindoring 8776a6b362 gmp(M02): add 3.3.5 input profile
Work-Package: M02-GMP-INPUT-PROFILE-335-001
Agent: sindo-main-codex
Tests: player input/movement/capability/terrain/camera/presentation, coordinate/streaming, docs/coordination and renderer dry-run passed
Fidelity: build-12340 binding hashes and selected defaults pinned; TrinityCore turn rate and WoWee mouse routing are compatibility references with explicit limits
2026-07-15 00:17:34 +04:00
sindoring c3094c9413 Merge pull request 'gmp(M02): gate debug movement capabilities' (#26) from work/sindo-main-codex/m02-sandbox-capabilities into master
Reviewed-on: #26
2026-07-14 22:58:54 +03:00
sindoring 7d5d13f702 docs(M02): publish movement capability handoff
Work-Package: M02-GMP-SANDBOX-CAPABILITIES-001

Agent: sindo-main-codex
2026-07-14 23:56:00 +04:00
sindoring 742c415885 gmp(M02): gate debug movement capabilities
Work-Package: M02-GMP-SANDBOX-CAPABILITIES-001

Agent: sindo-main-codex

Tests: capability/input/movement/terrain/camera/presentation; coordinate/streaming/docs/coordination; renderer dry-run 7/7

Fidelity: proves debug sprint/free-flight exclusion in Blizzlike335; no positive movement parity claim
2026-07-14 23:55:27 +04:00
sindoring 8c71e46872 Merge pull request 'rnd(M02): extract character presenters' (#25) from work/sindo-main-codex/m02-character-presentation into master
Reviewed-on: #25
2026-07-14 22:44:11 +03:00
79 changed files with 3520 additions and 298 deletions
+62 -1
View File
@@ -25,7 +25,8 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
## Основные файлы
- `src/scenes/streaming/streaming_world_loader.gd` - главный runtime streamer мира.
- `src/render/world_render_facade.gd` - стабильная граница runtime/tools для focus и metrics.
- `src/scenes/streaming/streaming_world_loader.gd` - внутренний runtime streamer мира.
- `src/scenes/streaming/eastern_kingdoms_streaming.tscn` - текущая сцена для проверки Azeroth/Eastern Kingdoms.
- `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids.
- `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material.
@@ -873,6 +874,66 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- This closes a coordinate/renderer integration criterion only. It does not
claim TrinityCore row equivalence or visual parity with the original client.
## 2026-07-15 WorldRenderFacade First Seam
- Runtime streaming scenes, checkpoint capture and renderer probes now submit
typed focus through `WorldRenderFacade`; those callers no longer call the
monolithic streamer's focus methods or configure its focus source directly.
- Baseline capture reads queue/cache/activity counts through
`renderer_metrics_snapshot()`. The facade returns a deep copy, so diagnostics
cannot mutate renderer-owned state.
- `StreamingWorldLoader` remains the internal implementation and still owns all
queues, tasks, caches, nodes and RIDs. The facade adds no parallel state or
replacement scheduler.
- This package does not alter targets, budgets, cache versions, placement,
visibility, materials or animation. Environment, entity visuals and ground
query remain explicit follow-up facade contracts in M03.
## 2026-07-15 StreamingTargetPlanner Extraction
- Runtime and editor wanted/retained ADT set calculation now lives in the
scene-free `StreamingTargetPlanner`.
- `StreamingTargetPolicy` preserves existing chunk/tile radii, warm/prewarm/
retain margins and the clamped boundary-prefetch threshold.
- `StreamingTargetPlan` freezes its focus/wanted/retained result; the streamer
still owns available paths, load-queue distance sorting, loaded-tile LOD/detail
state, async work, caches, nodes and RIDs.
- Asset-free fixtures cover center, edge/corner prefetch, catalog filtering,
clamp and editor behavior plus bounded High-like iteration timing.
- Cache versions, quality profiles, queue priority and visible rendering rules
are unchanged by the extraction.
## 2026-07-16 RenderBudgetScheduler Extraction
- The scene-free `RenderBudgetScheduler` now owns the per-frame counters for 16
named renderer operation lanes. It owns no queue, task, cache, Node or RID.
- `StreamingWorldLoader` snapshots its existing exported limits once per process
frame and asks for a boolean permit immediately before each historically
budgeted operation.
- Chunk removals and creations intentionally share one `chunk_geometry` lane;
the existing removal-first drain order and combined limit are preserved.
- Loader teardown cancels permit issuance before waiting for asynchronous work.
Existing worker/result staleness checks and resource cleanup remain unchanged.
- Asset-free contracts cover exact bounds, independent/shared lanes, frame reset,
invalid inputs, terminal cancellation, detached diagnostics and hot-path timing.
- Defaults, quality presets, queue order, cache versions and visible rendering
rules are unchanged. Asset-backed p95/p99 comparison remains required.
## 2026-07-16 Streamer Internal Access Gate
- `verify_renderer_internal_access.gd` derives private queue, task, cache and
tile-state field names from `StreamingWorldLoader` declarations and rejects
external member/reflection access in gameplay and Godot `EditorPlugin` package sources.
- The gate follows the implementation inventory instead of duplicating a manual
list, so a newly named private renderer field is covered automatically when it
matches those ownership categories.
- Scene composition may still reference the loader script and the facade's
implementation path; those are composition details, not mutable queue access.
- Renderer diagnostic probes are outside this gameplay/editor checklist.
`probe_render_terrain_height.gd` remains a documented temporary exception until
readiness and ground-query facade contracts replace its internal inspection.
- This is a source-only boundary check and changes no renderer behavior or fidelity.
## 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.
+1
View File
@@ -2,6 +2,7 @@
<!-- OPENWC_CLAIM:M02-GMP-INPUT-001:sindo-main-codex:2026-07-16 -->
<!-- OPENWC_HANDOFF:READY:M02-GMP-INPUT-001:6bd2e84 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M02-GMP-INPUT-001:14dead1 -->
## Ownership
@@ -0,0 +1,136 @@
# M02-GMP-INPUT-PROFILE-335-001 — WoW 3.3.5a input profile
<!-- OPENWC_CLAIM:M02-GMP-INPUT-PROFILE-335-001:sindo-main-codex:2026-07-17 -->
## Ownership
- Target: M02
- Program: GMP/QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m02-input-profile-335`
- Lease expires UTC: 2026-07-17
- Integrator: M02 milestone integrator
## Outcome
Add a profile-aware, remappable `Blizzlike335` input path whose default movement
bindings match the extracted build-12340 binding table while preserving the
existing `RenderSandbox` controls. Add deterministic turn intent and keyboard
turn consumption at the server-contract default turn rate.
## Non-goals
- Implement jump/fall/swim, network prediction or server reconciliation.
- Claim complete movement or camera parity from binding and turn-rate evidence.
- Persist user keybindings or build keybinding UI.
- Commit proprietary extracted client files.
- Mark M02 complete or edit its checklist/Evidence.
## Paths
- Exclusive: `src/tests/fixtures/player_input_profile_335.json`, this claim
- Shared/hotspots: `project.godot`, `src/domain/input/move_intent.gd`,
`src/gameplay/input/player_input_actions.gd`,
`src/gameplay/input/player_input_source.gd`,
`src/gameplay/movement/local_player_movement_controller.gd`,
`src/scenes/player/third_person_wow_controller.gd`,
`src/scenes/player/third_person_camera_rig.gd`, player/movement verifiers
and regression scene, `docs/modules/player-input.md`,
`docs/modules/local-player-movement.md`, `docs/modules/third-person-camera.md`
- Generated/ignored: local `.godot`, native DLL and proprietary extracted corpus
## Contracts and data
- `MoveIntent` gains an immutable, clamped signed turn axis.
- `PlayerInputSource` selects explicit sandbox or compatibility action sets at
construction and routes compatibility A/D to strafe while camera rotation is held.
- `LocalPlayerMovementController` converts turn intent into a deterministic yaw
delta without reading engine input or owning a scene transform.
- A sanitized JSON fixture records selected binding facts, source hashes and the
pinned TrinityCore 3.3.5 server-contract commit.
## Dependencies
- Requires: merged sandbox capability package on master `c3094c9`.
- Blocks: final M02 integration audit and milestone closeout.
- External state: local extracted build-12340 binding files are evidence inputs
only; committed tests are reproducible without them.
## Verification
- Fixture schema/provenance and selected default-binding regression.
- Pure profile composition, A/D turn, Q/E strafe and camera-held A/D strafe cases.
- Deterministic turn-rate and controller integration regression.
- Existing M02 and repository gates remain required.
## Documentation deliverables
- Updated input/movement public API, profile matrix, data flow, sequence,
failure/recovery, fidelity provenance and source maps.
## Simplicity and naming
- Important names: `turn_axis`, `BLIZZLIKE_STRAFE_LEFT`, `TURN_LEFT`.
- Simplest approach: one profile selection at composition plus one scalar intent.
- Rejected complexity: input-context framework, command bus and persisted binding store.
- Unavoidable complexity: compatibility A/D behavior depends on camera-rotate state.
- Measured optimization evidence: not applicable.
## Status
- State: ready-for-review
- Done: added distinct remappable profiles, signed turn intent/controller path,
camera-yaw synchronization, sanitized provenance fixture, regression coverage
and updated module specifications
- Next: milestone integrator reviews and merges `8776a6b`
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M02-GMP-INPUT-PROFILE-335-001:8776a6b -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M02-GMP-INPUT-PROFILE-335-001:eb122d5 -->
## Handoff
- Branch/commit: `work/sindo-main-codex/m02-input-profile-335` at `8776a6b`.
- Outcome: `RenderSandbox` retains A/D strafe and debug E/Q/Space/Shift behavior;
`Blizzlike335` now samples W/S and Up/Down forward/back, Q/E strafe, A/D and
Left/Right turn, with A/D routed to strafe while right-mouse rotation is active.
- Public contracts: `MoveIntent.turn_axis`; profile-selecting
`PlayerInputSource.new(profile_id)`; pure compatibility composition;
`LocalPlayerMovementController.calculate_yaw_delta_radians(...)`; camera yaw
synchronization after keyboard turn. Unknown input profiles fail closed to
`Blizzlike335`.
- Project configuration: four additive compatibility actions; existing action
names and sandbox mappings remain valid. W/S also gain the original Up/Down
secondary defaults.
- Verification: player input `actions=16 intent_cases=9 profiles=3 fixture=1
controller=1 regression_scene=3`; movement `cases=15 state_transitions=2`;
capabilities `profiles=3 sprint=2 flight=2 scenes=2`; terrain
`contract=4 interpolation=1 cache=1 failures=2`; camera
`state_cases=12 policy_cases=3`; presentation `appearance_cases=9
animation_cases=10`; coordinate boundaries `files=101`; StreamingFocus
`runtime_scenes=2 capture_tools=3`.
- Renderer: unified dry-run passed project/material/dedupe/cache/manifest/
calibration/coordinate/server-spawn gates and planned all seven checkpoints;
coordinate gate reported `files=103`. Missing proprietary GLB/DBC/ADT inputs
produced the expected diagnostics only.
- Repository gates: documentation `module_specs=8`; coordination
`fallback_claims=31`, with the same five unrelated expired M00 warnings.
- Fidelity: local build-12340 `DefaultBindings.wtf` SHA-256
`35FFA0E4E2356A13A0633A848F9E57ABE7DF9D8A16C2245E15D33013AD2F56A5`
and `Bindings.xml` SHA-256
`2E01276AFB7462F1417710F13B4BF1A456341F7F28CC8AE0D6D9C7442D7F6793`
were rechecked and selected facts sanitized into the committed fixture.
TrinityCore revision `2853a621d6af91a803787a2b8a509f4ce3e0300d` supplies the
3.141594 rad/s player server-contract turn rate. WoWee revision
`626243e937fb93965fa583a6507ed5a1aa7dda4b` supports right-mouse A/D routing.
- Fidelity limits: server turn rate and public reference routing are not direct
original-client timing/behavior proof. Jump, autorun and left-button
camera/select defaults are recorded but not implemented. This package does
not claim complete input, movement or camera parity.
- Migration/cache: no persisted settings or cache schema. Future saved bindings
must migrate stable action names; current project users receive additive actions.
- Documentation: updated input, movement and camera APIs, inputs/outputs, data
flow, sequence, ownership, failure/recovery, configuration, fidelity and source maps.
- Recommended merge order: merge after sandbox capabilities (already on master),
then run the final M02 integration/evidence audit without marking the target
done solely from this package.
@@ -2,6 +2,7 @@
<!-- OPENWC_CLAIM:M02-GMP-MOVEMENT-001:sindo-main-codex:2026-07-16 -->
<!-- OPENWC_HANDOFF:READY:M02-GMP-MOVEMENT-001:435e1c9 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M02-GMP-MOVEMENT-001:5206c42 -->
## Ownership
@@ -0,0 +1,127 @@
# M02-GMP-SANDBOX-CAPABILITIES-001 — Debug movement capability gate
<!-- OPENWC_CLAIM:M02-GMP-SANDBOX-CAPABILITIES-001:sindo-main-codex:2026-07-16 -->
## Ownership
- Target: M02
- Program: GMP/FND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m02-sandbox-capabilities`
- Lease expires UTC: 2026-07-16
- Integrator: M02 milestone integrator
## Outcome
Introduce an immutable local-player movement capability contract and require it
at the movement boundary so debug sprint and free flight execute only in the
explicit `RenderSandbox` profile and are rejected by `Blizzlike335`.
## Non-goals
- Claim that current movement values or `Blizzlike335` locomotion are 1:1.
- Build a global application/profile framework before the application shell exists.
- Remove debug Input Map actions or change their current sandbox bindings.
- Implement jump, fall, swim, collision, server authority or reconciliation.
- Mark M02 complete or edit its checklist/Evidence.
## Paths
- Exclusive: `src/gameplay/movement/player_movement_capabilities.gd`,
`src/tools/verify_player_movement_capabilities.gd`, this claim
- Shared/hotspots: `src/gameplay/movement/local_player_movement_controller.gd`,
`src/scenes/player/third_person_wow_controller.gd`, movement/input verifiers,
player regression scene, `docs/modules/local-player-movement.md`,
`docs/modules/player-input.md`
- Generated/ignored: local `.godot`, native DLL, generated resources and renderer corpus
## Contracts and data
- `PlayerMovementCapabilities` owns immutable profile ID plus explicit debug
sprint/flight booleans and safe named factories.
- `LocalPlayerMovementController` requires the capability value; missing input
falls back to `Blizzlike335`, never sandbox.
- Player scene maps its exported profile selection once at the composition boundary.
- Input actions and `MoveIntent` remain unchanged; rejected debug requests have no effect.
## Dependencies
- Requires: merged character presentation package on master `8c71e46`.
- Blocks: final M02 integration audit and milestone closeout.
- External state: none; verification is asset-free.
## Verification
- Pure profile factory/default and immutability regression.
- Sprint accepted in RenderSandbox and ignored in Blizzlike335.
- Flight toggle/vertical basis accepted in RenderSandbox and rejected in Blizzlike335.
- Real regression scene verifies both profile compositions.
- Existing input/movement/terrain/camera/presentation and renderer gates remain required.
- Fidelity evidence: negative proof that debug capabilities cannot affect the
compatibility profile; no positive movement parity claim.
## Documentation deliverables
- Inline public API and profile semantics on the capability/controller boundary.
- Updated movement and input specs: API, inputs/outputs, data flow, lifecycle,
failure/recovery, capability matrix and source map.
## Simplicity and naming
- Important name: `PlayerMovementCapabilities`.
- Simplest approach: one immutable value injected into the existing controller.
- Rejected complexity: autoload singleton, generic feature-flag service and global profile registry.
- Unavoidable complexity: one profile-to-capability mapping at scene composition.
- Measured optimization evidence: not applicable.
## Status
- State: ready-for-review
- Done: added immutable capability contract, gated sprint/free-flight, covered
pure and real-scene profiles and updated movement/input specifications
- Next: milestone integrator reviews and merges `742c415`
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M02-GMP-SANDBOX-CAPABILITIES-001:742c415 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M02-GMP-SANDBOX-CAPABILITIES-001:c3094c9 -->
## Handoff
- Branch/commit: `work/sindo-main-codex/m02-sandbox-capabilities` at `742c415`.
- Outcome: debug sprint and free flight now require explicit
`PlayerMovementCapabilities.render_sandbox()`; `Blizzlike335`, unknown and
null capability inputs reject both effects.
- Public contract: immutable profile ID plus `allows_debug_sprint` and
`allows_debug_free_flight`; named factories map configuration once at the
composition boundary. The movement controller exposes its effective profile ID.
- Runtime composition: `ThirdPersonWowController.movement_profile_id` defaults
to `RenderSandbox`, preserving renderer-scene behavior. Selecting
`Blizzlike335` keeps ordinary movement while ignoring Shift/Space/E/Q effects.
- Verification: capability `profiles=3 sprint=2 flight=2 scenes=2 boundary=1`,
movement `cases=12 state_transitions=2`, input `actions=12 intent_cases=6`,
terrain `contract=4 interpolation=1 cache=1 failures=2`, camera
`state_cases=12 policy_cases=3`, presentation
`appearance_cases=9 animation_cases=10`, coordinate boundaries
`files=101 consumers=5`, and StreamingFocus `runtime_scenes=2 capture_tools=3` passed.
- Renderer: dry-run passed project/material/dedupe/cache/manifest/calibration/
coordinate/server-spawn gates and planned all seven checkpoints; the combined
run reported coordinate `files=103`. Missing proprietary GLB/DBC/ADT inputs
produced the expected diagnostics only.
- Repository gates: documentation `module_specs=8`, coordination
`fallback_claims=30`; five unrelated expired M00 claim warnings remain.
- Fidelity: this package provides negative evidence that sandbox-only debug
movement cannot alter the `Blizzlike335` profile. It does not prove ordinary
movement, bindings, flight or timing match build 12340.
- Migration: direct `LocalPlayerMovementController.new(...)` callers that need
debug behavior must now pass `render_sandbox()`; omitted/null capability is
intentionally compatibility-safe. Existing repository scenes require no edit.
- Cache/persistence: no schema, cache, binding or stored-profile migration.
- Local ignored inputs: native extension DLL, UID cache and generated ADT
resource scripts supported renderer verification and are not committed.
- Risks: runtime renderer scenes still default to `RenderSandbox`; the future
application shell must explicitly choose its profile. `Blizzlike335` currently
guarantees only debug exclusion, not complete movement fidelity.
- Recommended merge order: merge after presentation (already on master), then
perform the final M02 integration/evidence audit before any target closeout.
- Documentation: updated movement/input API, inputs/outputs, data flow, state,
sequence, ownership, failure/recovery, capabilities, verification and source maps.
@@ -2,6 +2,7 @@
<!-- OPENWC_CLAIM:M02-GMP-TERRAIN-QUERY-001:sindo-main-codex:2026-07-16 -->
<!-- OPENWC_HANDOFF:READY:M02-GMP-TERRAIN-QUERY-001:a45d521 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M02-GMP-TERRAIN-QUERY-001:f5bb64e -->
## Ownership
@@ -0,0 +1,111 @@
# M02-QAR-INTEGRATOR-CLOSEOUT-001 — M02 milestone closeout
<!-- OPENWC_CLAIM:M02-QAR-INTEGRATOR-CLOSEOUT-001:sindo-main-codex:2026-07-17 -->
## Ownership
- Target: M02
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m02-integrator-closeout`
- Lease expires UTC: 2026-07-17
- Integrator: sindo-main-codex, continuing the user-directed milestone workflow on 2026-07-15
## Outcome
Audit every merged M02 package on current master, record final structural and
fidelity evidence, close M02 and activate M03 only when all target and repository
gates pass.
## Non-goals
- Change player, movement, terrain, camera, presentation or renderer behavior.
- Claim complete WoW 3.3.5a locomotion/camera parity.
- Implement recorded jump/fall/swim/fly, collision or timing gaps.
- Commit proprietary assets, generated UIDs, caches or user scene changes.
## Paths
- Exclusive: M02/M03 target status, M02 final Evidence, all M02 claim acceptance markers
- Shared/hotspots: `targets/README.md`, `targets/DEVELOPMENT_ROADMAP.md`, this claim
- Generated/ignored: local Godot imports, native DLL, renderer corpus and `user://` reports
## Contracts and data
- Runtime APIs, Input Map actions, fixture schemas and cache formats: unchanged
- Consumers: milestone sequence and future M03 renderer-facade packages
- Fidelity classification distinguishes direct build-12340 binding evidence,
server/reference compatibility evidence and explicitly unverified gaps
## Dependencies
- Requires: all seven M02 READY packages integrated through master `eb122d5`
- Blocks: M03 activation
- External state: private original-client files are hash/provenance inputs only
## Verification
- Commands: all M02 input/movement/capability/terrain/camera/presentation tests,
coordinate/streaming boundaries, unified renderer dry-run, documentation,
coordination and diff hygiene
- Fidelity evidence: build-12340 default binding hashes and selected facts;
pinned TrinityCore turn rate; public reference routing; explicit remaining gaps
- Performance: no runtime change; renderer baseline remains the performance guard
## Documentation deliverables
- M02 final target Evidence/checklist and limitation statement
- M02 claim integration-acceptance markers
- Target index and roadmap transition to M03
- Existing API/module specifications and diagrams audited without behavioral edits
## Simplicity and naming
- Important names introduced: none beyond the closeout work-package ID
- Simplest approach: evidence/status-only integrator patch
- Rejected complexity: behavioral fixes or a new closeout framework
- Unavoidable complexity: explicit evidence classification prevents false 1:1 claims
## Status
- State: ready-for-review
- Done: accepted seven M02 packages, reran post-merge gates, recorded complete
target Evidence, closed M02 and activated M03
- Next: merge closeout commit `1bf5b52` before starting an M03 implementation claim
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M02-QAR-INTEGRATOR-CLOSEOUT-001:1bf5b52 -->
## Handoff
- Commit: `1bf5b52`.
- Branch: `work/sindo-main-codex/m02-integrator-closeout`.
- Outcome: M02 is `DONE` with all eight structural checklist items and complete
classified Evidence; M03 is the only `ACTIVE` target and the target index/
roadmap point to renderer-facade extraction.
- Integration audit: all seven READY implementation commits are ancestors of
merged master `eb122d5`; their claims now carry the corresponding
`OPENWC_INTEGRATION:ACCEPTED` merge markers.
- Verification: input `16/9/3/1/3`, movement `15` cases and `2` transitions,
capability `3` profiles and `2` scenes, terrain `4/1/1/2`, camera `12/3`,
presentation `9/10/3`, coordinate boundaries `103` files and StreamingFocus
`2` runtime scenes/`3` capture tools all passed. Unified renderer dry-run
passed project/material/dedupe/shutdown gates, manifest `7/7/7`, calibration
maximum error `0.000015`, server-spawn renderer and seven checkpoint plans.
- Repository gates: documentation passed with `8` module specs and `7` required
files. Coordination passed with one active target; `13` expired M00 claim
warnings are historical and unrelated to M02.
- Fidelity: direct build-12340 binding hashes/facts are preserved in the
sanitized fixture. TrinityCore turn rate and WoWee mouse routing remain
explicitly reference-only. Translation speeds/camera values are sandbox
regressions, and jump/fall/swim/fly parity remains unimplemented/unverified.
Debug sprint/free flight is negatively proven absent from `Blizzlike335`.
- Remaining risks: target Evidence carries exact follow-up scope for M09/M10/M12:
movement/camera timing, jump/fall/swim/fly, autorun/LMB semantics, active
collision, terrain/liquid policy, persisted bindings, server authority and
prediction/reconciliation. No complete `1:1` claim is made.
- Runtime/cache/migration: no behavioral change, dependency, cache schema or
persisted settings migration in closeout.
- Documentation: M02 target Evidence/checklist, seven acceptance markers,
target index and roadmap state updated. Existing input, movement, terrain,
camera and presentation API/module specs and diagrams passed the documentation gate.
@@ -90,6 +90,7 @@ no-collision sandbox baseline.
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M02-RND-CAMERA-001:8c1cf9b -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M02-RND-CAMERA-001:68e6f60 -->
## Handoff
@@ -84,6 +84,7 @@ components without changing the current sandbox presentation.
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M02-RND-CHARACTER-PRESENTATION-001:97bb53f -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M02-RND-CHARACTER-PRESENTATION-001:8c71e46 -->
## Handoff
+117
View File
@@ -0,0 +1,117 @@
# M03-RND-FACADE-001 — Renderer facade first seam
<!-- OPENWC_CLAIM:M03-RND-FACADE-001:sindo-main-codex:2026-07-17 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-renderer-facade`
- Lease expires UTC: 2026-07-17
- Integrator: M03 milestone integrator
## Outcome
Introduce `WorldRenderFacade` as the only runtime/tool caller surface for typed
streaming focus updates and read-only renderer metrics, while retaining
`StreamingWorldLoader` as the unchanged internal implementation.
## Non-goals
- Extract the pure target planner or budget scheduler in this package.
- Add environment or entity-presentation commands before their contracts exist.
- Replace the M02 `TerrainQuery` gameplay boundary in this package.
- Change streaming radii, queues, cache formats, visuals or quality presets.
- Mark M03 complete or edit its checklist/Evidence.
## Paths
- Exclusive: `src/render/world_render_facade.gd`,
`src/tools/verify_world_render_facade.gd`, this claim
- Shared/hotspots: both runtime streaming scenes, three renderer capture/probe
tools, `src/tools/verify_streaming_focus.gd`, `docs/modules/world-renderer.md`,
`docs/modules/README.md`, `RENDER.md`
- Generated/ignored: local `.godot`, native build outputs, caches and proprietary
renderer corpus
## Contracts and data
- Public API: `set_streaming_focus`, `refresh_streaming_focus`,
`renderer_metrics_snapshot`
- Scene adapter: explicit `streaming_focus_source_path` owned by the facade
- Internal implementation seam: `streaming_world_loader_path`; callers must not
read streamer queues or call its methods directly
- Schema/cache/coordinate contracts: unchanged
- Consumers: runtime scenes, capture tool and renderer probes
## Dependencies
- Requires: M01 `StreamingFocus` and merged M02 closeout on master `4f566b1`
- Blocks: environment/entity/ground facade contracts and planner extraction
- External state: none; contract verification uses a test double
## Verification
- Commands: dedicated facade verifier, streaming-focus regression, renderer
baseline manifest/dry-run, coordinate boundary and repository gates
- Fixtures: typed focus, source Node3D and read-only metrics test double
- Fidelity evidence: no renderer configuration, planner, queue, placement,
visibility, material, animation or cache behavior changes
- Performance budget: one source position sample per process frame; target
recomputation remains throttled inside the existing streamer
## Documentation deliverables
- Inline public API docs for facade properties and methods
- Updated world-renderer module specification
- Updated data-flow, sequence and dependency diagrams
- Updated source map/status and renderer notes
## Simplicity and naming
- Important names: `WorldRenderFacade`, `renderer_metrics_snapshot`
- Simplest approach: one small Node adapter delegating to the existing streamer
- Rejected complexity: service container, generic command bus, streamer rewrite
or duplicate queue/cache state
- Unavoidable complexity: scene-relative source and implementation paths are
resolved on the main thread with explicit diagnostics
- Measured optimization evidence: not applicable; behavior-preserving seam
## Status
- State: ready-for-review
- Done: introduced the facade, migrated two runtime scenes and three renderer
tools, added regression coverage and updated renderer documentation
- Next: M03 integrator reviews and merges `3d528e3`
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-FACADE-001:3d528e3 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-FACADE-001:a100ced -->
## Handoff
- Commit: `3d528e3`
- Integration: accepted in master merge `a100ced`
- Results: `WorldRenderFacade` is the runtime/tool surface for typed focus and
detached metrics; the existing streamer remains the sole owner of queues,
caches, tasks, nodes and RIDs.
- Verification: facade `delegation=3 runtime_scenes=2 tools=3`; streaming focus
`contract=1 runtime_scenes=2 capture_tools=3`; coordinate boundary
`files=105 native_boundary=1 oracle=1 consumers=5 classifier_cases=6`;
render manifest `checkpoints=7 coverage=7 caches=7`; seven-checkpoint headless
dry-run; coordination/documentation/diff gates passed.
- Fidelity: no changes to renderer target calculation, budgets, queue order,
cache versions, placement, visibility, materials or animation. The dry-run
retained all seven checkpoint plans. No build-12340 parity claim is added.
- External/local inputs: ignored native DLL and generated ADT resource scripts
were copied from master only to run the worktree smoke. Proprietary extracted
ADT and character GLB data were unavailable, producing the expected dry-run
degraded-data diagnostics.
- Remaining risks: environment, entity visuals and ground query are not yet
facade contracts; streamer exported configuration remains transitional scene
composition; focus-source sampling adds one small main-thread adapter step per
process frame while target refresh remains streamer-throttled.
- Documentation updated: inline facade API, `docs/modules/world-renderer.md`
public API/inputs/outputs/data-flow/sequence/ownership/recovery/status/source
map, and `RENDER.md` file map plus implementation note.
@@ -0,0 +1,106 @@
# M03-RND-INTERNAL-ACCESS-GATE-001 — Streamer internal access gate
<!-- OPENWC_CLAIM:M03-RND-INTERNAL-ACCESS-GATE-001:sindo-main-codex:2026-07-18 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-streamer-internal-access-gate`
- Lease expires UTC: 2026-07-18
- Integrator: M03 milestone integrator
## Outcome
Add an automated repository boundary gate proving gameplay code and Godot
`EditorPlugin` implementations do not access `StreamingWorldLoader` private
queue, task, cache or tile-state fields.
## Non-goals
- Change renderer behavior, queues, jobs, cache formats or facade contracts.
- Ban scene composition references to the loader script or facade adapter path.
- Migrate renderer-only diagnostic probes that inspect internal readiness/terrain;
those require the later metrics/ground-query facade contract.
- Mark M03 complete.
## Paths
- Exclusive: `src/tools/verify_renderer_internal_access.gd`, this claim
- Shared/hotspots: `docs/modules/world-renderer.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Read-only scan scope: `src/gameplay/**/*.gd`, `src/editor/**/*.gd`, Godot
`EditorPlugin` package scripts under `src/**/*.gd` and `addons/**/*.gd`, and
the streamer field declarations
## Contracts and data
- Input: repository GDScript source and actual private streamer declarations
- Output: deterministic pass/fail diagnostics with consumer path and symbol
- Runtime, scene, cache, coordinate and asset contracts: unchanged
## Dependencies
- Requires: integrated facade and scheduler master `a023d76`
- Blocks: safe service extraction and M03 dependency regression acceptance
- External state: none
## Verification
- Commands: dedicated internal-access gate, facade regression, coordination,
documentation and diff gates
- Fixtures: gameplay source discovery, EditorPlugin discovery and synthetic
forbidden symbol detection
- Fidelity evidence: source-only enforcement; no runtime or visual code changes
- Performance budget: one bounded repository source scan in a headless test process
## Documentation deliverables
- World-renderer boundary/verification/known-gap/source-map updates
- Renderer implementation note documenting diagnostic-tool exception
- No new module spec because this package adds a verifier to the existing renderer module
## Simplicity and naming
- Important name: `verify_renderer_internal_access.gd`
- Simplest approach: derive forbidden fields from the current streamer instead
of maintaining a second hand-written queue list
- Rejected complexity: language plugin, custom linter framework or runtime proxy
- Unavoidable complexity: recursive source discovery expands each actual
`EditorPlugin` root to its package scripts while excluding reference mirrors
## Status
- State: ready-for-review
- Done: derived 43 private renderer symbols, scanned seven gameplay and nine
EditorPlugin-package sources, added synthetic positive/negative fixtures and docs
- Next: M03 integrator reviews and merges `b58bf2c`
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-INTERNAL-ACCESS-GATE-001:b58bf2c -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-INTERNAL-ACCESS-GATE-001:7e35de7 -->
## Handoff
- Commit: `b58bf2c`
- Results: repository gate derives private queue/task/cache/state declarations
from the streamer and rejects external member/reflection access in gameplay and
EditorPlugin package sources. Current scope passes with `43/7/9` inventory counts.
- Verification: internal access, facade and budget scheduler headless contracts;
Godot editor scan with local ignored dependencies; coordination, documentation
and diff gates passed.
- Fidelity: source-only enforcement changes no runtime configuration, queue order,
cache version, placement, material, animation or visibility behavior. No
build-12340 parity claim is added.
- External/local inputs: ignored native DLL and two generated ADT resource scripts
were copied from master only for facade/editor smoke and then removed. Proprietary
extracted world data was unavailable, producing expected degraded-data diagnostics.
- Remaining risks: renderer diagnostic terrain probe remains an explicit exception;
whitespace-obfuscated dynamic reflection is outside the lightweight source gate;
future non-EditorPlugin authoring code is already prohibited by repository policy.
- Documentation: world-renderer boundary/verification/capability/known-gap/source
map and `RENDER.md` implementation note updated; no new module was introduced.
- Integration: accepted in master merge `7e35de7`; post-merge access gate
`private_symbols=43 gameplay=7 editor_sources=9`, facade/scheduler/planner and
repository gates passed.
@@ -0,0 +1,116 @@
# M03-RND-SCHEDULER-001 — Bounded render budget scheduler
<!-- OPENWC_CLAIM:M03-RND-SCHEDULER-001:sindo-main-codex:2026-07-18 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-render-budget-scheduler`
- Lease expires UTC: 2026-07-18
- Integrator: M03 milestone integrator
## Outcome
Extract the streamer's per-frame main-thread operation quotas into a scene-free
`RenderBudgetScheduler`, preserving existing queue priority and quota values and
stopping permit issuance after renderer teardown cancellation.
## Non-goals
- Move queue, worker-task, cache, Node, Resource or RID ownership out of the streamer.
- Change queue ordering, concurrency limits, exported defaults or visual behavior.
- Add a generic job framework, priority graph, time-based adaptive budgeting or dependency.
- Change cache formats, renderer profiles or target milestone status.
## Paths
- Exclusive: `src/render/streaming/render_budget_scheduler.gd`,
`src/tools/verify_render_budget_scheduler.gd`,
`docs/modules/render-budget-scheduler.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated cache resources and
proprietary renderer corpus
## Contracts and data
- Input: immutable per-frame map from documented operation lane IDs to
non-negative operation limits
- Output: boolean permits and remaining/consumed diagnostic snapshots
- Lifecycle: `begin_frame` resets permits; `cancel` permanently rejects permits
for the current scheduler instance
- Queue contents, ordering, task concurrency and schema/cache versions: unchanged
- Consumer: `StreamingWorldLoader` main-thread queue/finalize drains
## Dependencies
- Requires: integrated M03 facade and streaming planner on master `650d584`
- Blocks: renderer performance regression gate and per-service extraction
- External state: none; tests use synthetic lane limits and no world assets
## Verification
- Commands: dedicated scheduler verifier, facade/planner/focus/coordinate
regressions, renderer manifest/dry-run and repository gates
- Fixtures: shared lane priority, independent lanes, zero/negative limits,
frame reset, unknown lane and cancellation
- Fidelity evidence: operation defaults and loop order are captured before
migration; all seven renderer checkpoint plans remain valid
- Performance budget: permit checks are constant-time dictionary operations and
create no Node, Resource, worker task, cache entry or RID
## Documentation deliverables
- Inline API docs for scheduler lifecycle, permits and diagnostics
- New module specification with input/output table, data-flow, sequence and
lifecycle diagrams
- Updated world-renderer flow/ownership/source map, module registry and renderer notes
## Simplicity and naming
- Important name: `RenderBudgetScheduler`
- Simplest approach: one small main-thread service with named quota lanes
- Rejected complexity: generic job graph, callbacks, queue ownership, adaptive
timing controller, service container or third-party dependency
- Unavoidable complexity: chunk removal and creation intentionally share one
lane so removal retains priority while consuming the same historical budget
- Measured optimization evidence: no optimization claim; exact bounded extraction
## Status
- State: ready-for-review
- Done: extracted all 16 historical operation quotas, migrated ordered drains,
added terminal teardown cancellation, asset-free verification and module docs
- Next: M03 integrator reviews and merges `e52f703`
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-SCHEDULER-001:e52f703 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-SCHEDULER-001:7ece2ab -->
## Handoff
- Commit: `e52f703`
- Results: `RenderBudgetScheduler` owns only per-frame lane counters and terminal
cancellation; `StreamingWorldLoader` retains queue order, task/cache/node/RID
ownership and operation execution. Chunk removal/create share one historical budget.
- Verification: scheduler `cases=6 iterations=20000 elapsed_ms=7.495`; planner
`cases=5 average_ms=1.879`; facade/focus contracts passed; coordinate boundary
`files=111 consumers=6`; renderer manifest `checkpoints=7 coverage=7 caches=7`;
shutdown ownership, documentation, coordination and diff gates passed.
- Fidelity: exported defaults, quality presets, queue order, operation sites,
cache versions and visible rendering rules are unchanged. Seven checkpoint
plans remain valid. No build-12340 parity claim is added.
- External/local inputs: ignored native DLL and two generated ADT resource scripts
were copied from master only for the worktree parse/smoke and then removed.
Proprietary extracted ADT data was unavailable, producing expected degraded-data diagnostics.
- Remaining risks: queue storage length and worker concurrency remain streamer-owned;
cancellation stops permit issuance but does not interrupt in-flight work; exact
p95/p99 impact still requires an asset-backed baseline.
- Documentation: new scheduler module spec with API, input/output, data-flow,
sequence, lifecycle, ownership, cancellation, performance and source-map sections;
world renderer, module registry and renderer implementation notes updated.
- Integration: accepted in master merge `7ece2ab`; post-merge scheduler timing
`cases=6 iterations=20000 elapsed_ms=10.216`, all listed regressions and gates passed.
@@ -0,0 +1,120 @@
# M03-RND-STREAMING-PLANNER-001 — Pure streaming target planner
<!-- OPENWC_CLAIM:M03-RND-STREAMING-PLANNER-001:sindo-main-codex:2026-07-17 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-streaming-target-planner`
- Lease expires UTC: 2026-07-17
- Integrator: M03 milestone integrator
## Outcome
Extract runtime/editor ADT wanted/retained-set calculation and boundary prefetch
selection from `StreamingWorldLoader` into a scene-free, deterministic
`StreamingTargetPlanner` with typed focus input and asset-free contract tests.
## Non-goals
- Change load/retain radii, boundary thresholds, queue priority or LOD policy.
- Move queue mutation, async jobs, cache ownership or GPU finalization.
- Extract `RenderBudgetScheduler` or asset services in this package.
- Change cache formats, renderer profiles or target milestone status.
## Paths
- Exclusive: `src/render/streaming/streaming_target_planner.gd`,
`src/render/streaming/streaming_target_policy.gd`,
`src/render/streaming/streaming_target_plan.gd`,
`src/tools/verify_streaming_target_planner.gd`,
`docs/modules/streaming-target-planner.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`src/tools/verify_coordinate_conversion_boundaries.gd`,
`docs/modules/world-renderer.md`, `docs/modules/coordinate-mapping.md`,
`docs/modules/README.md`, `RENDER.md`
- Generated/ignored: `.godot`, native DLL, generated cache resources and
proprietary renderer corpus
## Contracts and data
- Input: immutable `GodotWorldPosition`, available-tile key set and immutable
`StreamingTargetPolicy`
- Output: immutable `StreamingTargetPlan` with focus tile plus detached
wanted/retained tile-key sets
- Tile-key representation remains the streamer's existing `<x>_<y>` internal key
- Schema/cache/coordinate versions: unchanged
- Consumers: `StreamingWorldLoader` runtime/editor refresh paths
## Dependencies
- Requires: M01 typed coordinates/focus and integrated facade package `a100ced`
- Blocks: budget scheduler extraction and planner performance regression gate
- External state: none; tests use synthetic tile catalogs
## Verification
- Commands: dedicated planner verifier, facade/focus/coordinate regressions,
renderer manifest/dry-run and repository gates
- Fixtures: center, edge, corner, unavailable-tile, clamped-radius and editor cases
- Fidelity evidence: expected tile sets are captured from current formulas; full
renderer dry-run retains all seven checkpoint plans
- Performance budget: planner work remains bounded by square radii and creates no
Node, Resource, task, cache or RID
## Documentation deliverables
- Inline API docs for planner/policy/plan
- New module specification with input/output table and data-flow diagram
- Updated world-renderer data-flow/sequence/ownership/source map
- Updated module registry and renderer notes
## Simplicity and naming
- Important names: `StreamingTargetPlanner`, `StreamingTargetPolicy`,
`StreamingTargetPlan`
- Simplest approach: one pure calculator plus two immutable value objects
- Rejected complexity: generic graph planner, priority framework, service
container, job ownership or duplicate available-tile catalog
- Unavoidable complexity: boundary-prefetch cross product must preserve corner
behavior and existing string tile keys
- Measured optimization evidence: no new optimization; exact extraction
## Status
- State: ready-for-review
- Done: extracted immutable policy/plan and pure planner, migrated runtime/editor
target refresh, added behavior/dependency/performance coverage and module docs
- Next: integrated in master; next M03 package extracts the budget scheduler
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-STREAMING-PLANNER-001:ad8fc41 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-STREAMING-PLANNER-001:643850c -->
## Handoff
- Commit: `e887c3b`; cold-start fix `ad8fc41`
- Integration: accepted in master merges `d0f74f8` and `643850c`
- Results: runtime/editor wanted and retained ADT sets plus boundary-prefetch
centers are calculated by scene-free `StreamingTargetPlanner`; streamer keeps
queue ordering, LOD/detail state, jobs, caches, nodes and RIDs.
- Verification: planner `cases=5 iterations=250 average_ms=1.765`; facade
`delegation=3 runtime_scenes=2 tools=3`; focus `contract=1 runtime_scenes=2
capture_tools=3`; coordinate boundary `files=109 consumers=6`; renderer
manifest `checkpoints=7 coverage=7 caches=7`; shutdown ownership passed;
documentation, coordination and diff gates passed.
- Fidelity: center/corner/catalog/clamp/editor fixtures preserve the extracted
formulas; seven-checkpoint dry-run retained all plans. No radius, threshold,
load priority, cache version, placement, material, animation or visibility
rule changed. No build-12340 parity claim is added.
- External/local inputs: ignored native DLL and generated ADT resource scripts
were copied from master for worktree smoke only. Proprietary ADT and character
assets were unavailable, so the dry-run emitted expected degraded-data diagnostics.
- Remaining risks: exact p95/p99 impact still needs an asset-backed baseline;
pathological custom radii are not benchmarked; tile-key strings remain the
existing internal catalog representation; queue/LOD application remains monolithic.
- Documentation updated: new planner module spec with API/input/output/data-flow/
sequence/ownership/recovery/performance/source-map sections; world renderer,
coordinate mapping, module registry and renderer implementation notes updated.
+2
View File
@@ -15,6 +15,8 @@
| 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) |
| 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) |
| Gameplay | Prototype/Planned | [`../../targets/roadmap/04-gameplay-systems.md`](../../targets/roadmap/04-gameplay-systems.md) |
| UI/Lua/audio | Prototype/Planned | [`../../targets/roadmap/05-ui-lua-audio.md`](../../targets/roadmap/05-ui-lua-audio.md) |
+4
View File
@@ -116,9 +116,12 @@ flowchart LR
Godot -->|typed direct adapter| Tile
Godot -->|typed direct adapter| Local
Godot --> TerrainQuery[AdtTerrainQuery consumer]
Godot --> TargetPlanner[StreamingTargetPlanner consumer]
TerrainQuery --> Tile
TerrainQuery --> Local
TerrainQuery --> Chunk
TargetPlanner --> Tile
TargetPlanner --> Local
Tile -->|tile plus local| Godot
ServerYaw[ServerWorldYaw] -->|normalize only| CanonicalYaw[CanonicalWowWorldYaw]
CanonicalYaw -->|same numeric world angle| GodotYaw[GodotWorldYaw]
@@ -249,6 +252,7 @@ raw three-number arrays are not an accepted new persistence contract.
| `src/tools/verify_coordinate_golden_fixtures.gd` | Cross-source fixture schema and mapper validation |
| `src/tools/verify_coordinate_conversion_boundaries.gd` | Repository-wide manual conversion and required-consumer gate |
| `src/gameplay/terrain/adt_terrain_query.gd` | Typed Godot-position consumer for ADT tile/local/chunk lookup |
| `src/render/streaming/streaming_target_planner.gd` | Typed Godot-position consumer for ADT focus tile and boundary-local lookup |
| `src/tools/server_spawn_render_manifest.json` | Pinned server-spawn camera, player, marker and expected ADT ownership |
| `src/tools/verify_server_spawn_renderer.gd` | Fixture-to-renderer manifest contract verifier |
| `docs/adr/0001-canonical-world-coordinates.md` | Normative axes, units, boundaries and rollout decision |
+60 -15
View File
@@ -5,17 +5,18 @@
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M02 / M02-GMP-MOVEMENT-001, camera basis and character-presentation consumer updates |
| Target/work package | M02 / M02-GMP-MOVEMENT-001, M02-GMP-SANDBOX-CAPABILITIES-001, M02-GMP-INPUT-PROFILE-335-001 and presentation consumers |
| Owners | Gameplay local movement state; scene composition owns the instance |
| Last verified | `97bb53f`, 2026-07-14 |
| Profiles/capabilities | Current render sandbox; production/debug profile gate pending |
| Last verified | M02-GMP-INPUT-PROFILE-335-001 worktree, 2026-07-15 |
| Profiles/capabilities | `RenderSandbox` debug movement; `Blizzlike335` selected bindings and keyboard turn with debug sprint/flight rejected |
## Purpose
Own current local velocity, acceleration and sandbox flight state independently
of the player scene. Convert an immutable `MoveIntent` plus an explicit
Godot-world movement basis into deterministic per-tick displacement without
querying terrain, cameras, input devices or visual assets.
Godot-world movement basis into deterministic per-tick displacement and convert
signed turn intent into a yaw delta without querying terrain, cameras, input
devices or visual assets.
## Non-goals
@@ -32,7 +33,10 @@ flowchart LR
Input[MoveIntent] --> Movement[LocalPlayerMovementController]
Basis[Explicit local movement Basis] --> Movement
Delta[Physics delta seconds] --> Movement
Profile[Scene profile ID] --> Capabilities[PlayerMovementCapabilities]
Capabilities --> Movement
Movement --> Displacement[Godot-world displacement]
Movement --> Yaw[Godot yaw delta radians]
Movement --> Velocity[Read-only velocity state]
Scene[ThirdPersonWowController] --> Basis
Displacement --> Scene
@@ -57,11 +61,16 @@ Forbidden dependencies:
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `LocalPlayerMovementController.new(...)` | Constructor | Sets explicit speeds, acceleration and sandbox sprint multiplier | Created by composition root; retained for player scene lifetime | Caller owns configuration validation |
| `PlayerMovementCapabilities.render_sandbox()` | Factory | Creates the only debug sprint/free-flight capability set | Immutable caller value | None |
| `PlayerMovementCapabilities.blizzlike_335()` | Factory | Creates the compatibility-safe set with debug movement disabled | Immutable caller value | Name is intent, not a parity claim |
| `PlayerMovementCapabilities.for_profile_id(...)` | Factory | Maps composition configuration once; unknown IDs fail closed | Immutable caller value | Unknown maps to `Blizzlike335` |
| `LocalPlayerMovementController.new(...)` | Constructor | Sets explicit speeds, acceleration, sprint multiplier and movement capabilities | Created by composition root; retained for player scene lifetime | Null capability defaults to `Blizzlike335` |
| `godot_world_velocity_units_per_second` | Read-only state | Current Godot-world velocity after the last update | Stable until next mutation on owning thread | None |
| `is_flight_enabled` | Read-only state | Current sandbox free-flight mode | Stable until toggle | None |
| `toggle_sandbox_flight()` | Command | Toggles flight and clears velocity | Owning physics/input thread | Returns new flight state |
| `movement_profile_id` | Read-only state | Effective capability profile ID | Controller lifetime | None |
| `toggle_sandbox_flight()` | Command | Toggles flight only when explicitly permitted and clears velocity | Owning physics/input thread | Rejected request returns `false` with state unchanged |
| `advance(move_intent, godot_world_movement_basis, delta_seconds)` | Stateful update | Accelerates toward requested velocity and returns displacement for the tick | Owning physics thread; one call per tick | Negative delta is treated as zero |
| `calculate_yaw_delta_radians(move_intent, delta_seconds)` | Pure query | Converts signed turn input to Godot yaw delta | Owning physics thread | Negative delta is treated as zero |
Constructor units:
@@ -73,16 +82,19 @@ Constructor units:
| `flight_vertical_speed_units_per_second` | Sandbox vertical Godot units per second |
| `acceleration_units_per_second_squared` | Velocity change per second |
| `sandbox_sprint_multiplier` | Dimensionless debug multiplier |
| `keyboard_turn_speed_radians_per_second` | Positive keyboard yaw speed; defaults to PI rad/s |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | `MoveIntent` | `PlayerInputSource` | Movement controller | Immutable caller-held value | One physics tick |
| Input | `PlayerMovementCapabilities` | Application/player scene composition | Movement controller | Immutable caller-held value | Controller lifetime |
| Input | Godot-world `Basis` | Player or `ThirdPersonCameraRig` adapter | Movement controller | Value copy | One physics tick |
| Input | Delta seconds | Godot physics loop | Movement controller | Scalar value | One physics tick |
| Input | Flight toggle command | Remappable sandbox action via scene adapter | Movement controller | Synchronous command | Input event dispatch |
| Output | Displacement `Vector3` | Movement controller | Player scene adapter | Value copy | Applied in same tick |
| Output | Yaw delta radians | Movement controller | Player scene adapter | Scalar value | Applied before movement basis selection |
| Output | Velocity `Vector3` | Movement controller | Facing and `CharacterAnimationPresenter` scene adapter | Read-only value copy | Until next update |
| Output | Flight state | Movement controller | Basis/terrain scene adapter | Read-only boolean | Until next toggle |
@@ -97,9 +109,14 @@ Side effects:
```mermaid
flowchart LR
Intent[MoveIntent axes] --> Target[Calculate target velocity]
Intent --> Turn[Calculate signed yaw delta]
Turn --> SceneYaw[Scene applies character yaw]
Basis[Player or camera-pivot Basis] --> Directions[Forward/right directions]
Directions --> Target
Config[Speeds and sprint multiplier] --> Target
Profile[RenderSandbox or Blizzlike335] --> Capability[Immutable debug capabilities]
Capability -->|allow or reject sprint| Target
Capability -->|allow or reject flight toggle| Current
Previous[Owned velocity] --> Integrate[move_toward by acceleration × delta]
Target --> Integrate
Integrate --> Current[Updated velocity]
@@ -112,7 +129,8 @@ flowchart LR
```mermaid
stateDiagram-v2
[*] --> GroundedSandbox
GroundedSandbox --> FlyingSandbox: toggle / clear velocity
GroundedSandbox --> FlyingSandbox: toggle [debug flight allowed] / clear velocity
GroundedSandbox --> GroundedSandbox: toggle [debug flight rejected]
FlyingSandbox --> GroundedSandbox: toggle / clear velocity
GroundedSandbox --> GroundedSandbox: advance
FlyingSandbox --> FlyingSandbox: advance
@@ -128,8 +146,14 @@ sequenceDiagram
participant Input as PlayerInputSource
participant Scene as ThirdPersonWowController
participant Move as LocalPlayerMovementController
participant Cap as PlayerMovementCapabilities
participant Present as CharacterAnimationPresenter adapter
Input-->>Scene: MoveIntent
Scene->>Cap: map configured profile once
Scene->>Move: construct with immutable capabilities
Scene->>Move: calculate_yaw_delta_radians(intent, delta)
Move-->>Scene: yaw delta
Scene->>Scene: apply yaw and synchronize camera orbit yaw
Scene->>Move: advance(intent, selected basis, delta)
Move-->>Scene: displacement
Scene->>Scene: apply global position and ground snap
@@ -140,6 +164,8 @@ sequenceDiagram
## Ownership, threading and resources
- The player scene creates and exclusively owns one movement controller.
- The composition root creates one immutable capability value; the controller
retains it and exposes only its effective profile ID.
- Velocity and flight state have the same lifetime as that controller.
- All mutations currently occur on the main input/physics thread.
- Read-only value returns are copies; consumers cannot mutate owned state.
@@ -153,6 +179,8 @@ sequenceDiagram
| Negative physics delta | `advance` guard | Returns zero displacement; velocity unchanged | Unit regression | Resume with valid non-negative delta |
| Missing scene basis owner | Scene composition defect | Controller cannot be called correctly | Scene/parser error or boundary test | Restore explicit scene dependency |
| Flight toggle during motion | Command path | Velocity clears before next step | State-transition regression | Continue from zero velocity |
| Flight/sprint request in `Blizzlike335` | Capability check | Request has no effect | Profile regression | Select explicit `RenderSandbox` only for tooling/debug scenes |
| Unknown/null profile capability | Factory/constructor safe default | Uses `Blizzlike335`; debug movement disabled | Capability regression | Correct composition profile ID |
| Missing terrain data | `TerrainGroundSample.is_available == false` | Scene skips existing ground snap | Stable terrain failure code | Replace/recover query backend |
There is no asynchronous cancellation, retry or rollback. Recreating the
@@ -169,6 +197,10 @@ controller resets velocity and flight state deterministically.
| Flight vertical speed | `7.0` units/s | Sandbox debug | Fixed for controller lifetime | Vertical free-flight target |
| Sprint multiplier | `6.0` | Sandbox debug | Fixed for controller lifetime | Multiplies planar and vertical targets |
| Camera-relative basis | Selected only while flight is enabled | Sandbox debug | Per tick | Preserves existing pitched flight direction |
| Debug sprint permission | Enabled | `RenderSandbox` only | Immutable | Allows sprint request multiplier |
| Debug free-flight permission | Enabled | `RenderSandbox` only | Immutable | Allows toggle and vertical movement |
| Compatibility-safe debug permissions | Disabled | `Blizzlike335` | Immutable | Ignores sprint/toggle requests |
| Keyboard turn rate | `3.141594` rad/s | `Blizzlike335` composition | Fixed for controller lifetime | Server-contract default applied to signed turn intent |
## Persistence, cache and migration
@@ -187,13 +219,17 @@ separate versioned movement snapshot contract.
- Unit/contract: `src/tools/verify_local_player_movement.gd` covers forward,
backward, strafe, rotated basis, acceleration, deceleration, sprint, flight,
pitched camera basis, state reset and invalid delta.
pitched camera basis, state reset, signed keyboard turn and invalid delta.
- Capability: `verify_player_movement_capabilities.gd` covers three profile-ID
cases, safe defaults, sprint/flight allow/reject behavior and two real scene compositions.
- Boundary: verifier rejects Node/Input/Camera/ADT dependencies and movement
integration/state remaining in the player scene.
- Scene integration: `verify_player_input.gd` drives the asset-free player
regression scene through forward motion and immediate flight toggle.
- Fidelity evidence: default values and formulas are regression-locked against
the pre-extraction sandbox. No original-client comparison is claimed.
- Fidelity evidence: translation defaults remain regression-locked against the
pre-extraction sandbox. The compatibility keyboard turn default is pinned to
TrinityCore 3.3.5 `playerBaseMoveSpeed[MOVE_TURN_RATE]`; that server contract
does not alone prove original-client timing parity.
- Performance budget: constant-time vector math, no per-tick I/O or scene lookup;
`MoveIntent` allocation remains owned by the input package.
@@ -202,7 +238,8 @@ separate versioned movement snapshot contract.
- Terrain/collision policy can consume displacement without changing input or velocity ownership.
- A future server-aware predictor may replace this controller behind the scene composition boundary.
- A typed movement snapshot may expose deterministic replay state when M08/M09 require it.
- Sandbox debug capability gating can reject sprint/flight before commands reach this controller.
- A future application profile can map to this narrow capability value without
coupling the movement controller to the application shell.
## Capability status
@@ -210,7 +247,9 @@ separate versioned movement snapshot contract.
|---|---|---|---|
| Scene-free local velocity state | Implemented | Pure controller and source boundary regression | Integrate target checklist after review |
| Existing planar acceleration/speeds | Implemented | Exact numeric unit cases and real scene regression | Compare with build 12340 before fidelity claim |
| Existing camera-relative free flight | Implemented | Pitched-basis and toggle/reset cases | Restrict to sandbox profile |
| Compatibility keyboard turn | Implemented | Pinned server-core value, signed pure cases and real-scene profile regression | Direct original-client timing comparison remains required |
| Existing camera-relative free flight | Implemented in `RenderSandbox` | Pitched-basis, profile and toggle/reset cases | Original-client flight semantics remain out of scope |
| Typed sprint/free-flight exclusion | Implemented | Pure and real-scene profile regressions | Application shell must select profile explicitly later |
| Terrain height query | Implemented | Typed `TerrainQuery` and injected player regression | Ground-snap policy remains scene-owned |
| Terrain collision movement policy | Planned | Height-only query does not model collision | Add slopes/holes/collision later |
| Jump/fall/swim | Planned | M02/M09 roadmap | Requires terrain/liquid and server contracts |
@@ -218,12 +257,15 @@ separate versioned movement snapshot contract.
## Known gaps and risks
- Numeric defaults preserve the existing sandbox but are not original-client evidence.
- Translation/acceleration defaults preserve the existing sandbox and are not original-client evidence.
- Configuration is captured at scene `_ready`; runtime mutation of exported speed
properties does not reconfigure an existing controller.
- Ground snap remains in the player scene, but its ADT data access is now behind
a replaceable `TerrainQuery`.
- Sprint and free flight still lack a typed sandbox capability gate.
- Runtime renderer scenes intentionally default to `RenderSandbox`; a future
game-client composition must explicitly select `Blizzlike335`.
- `Blizzlike335` currently means only debug movement exclusion and must not be
interpreted as evidence that remaining movement semantics are 1:1.
- The module uses Godot math value types; a future engine-independent gameplay
predictor may require scalar/domain movement contracts.
@@ -232,13 +274,16 @@ separate versioned movement snapshot contract.
| Path | Responsibility |
|---|---|
| `src/gameplay/movement/local_player_movement_controller.gd` | Owned velocity/flight state and deterministic integration |
| `src/gameplay/movement/player_movement_capabilities.gd` | Immutable profile-to-debug-capability contract and safe defaults |
| `src/gameplay/terrain/terrain_query.gd` | Replaceable ground-height input boundary |
| `src/scenes/player/third_person_camera_rig.gd` | Camera-relative sandbox flight basis producer |
| `src/scenes/character/character_animation_presenter.gd` | Locomotion presentation consumer behind the scene adapter |
| `src/scenes/player/third_person_wow_controller.gd` | Composition, basis selection, displacement/terrain and presentation-state adapters |
| `src/tools/verify_local_player_movement.gd` | Pure numeric, state-transition and dependency regression |
| `src/tools/verify_player_movement_capabilities.gd` | Profile factory, allow/reject and real-scene regression |
| `src/tests/scenes/player_input_regression.tscn` | Asset-free integrated player fixture |
| `src/tools/verify_player_input.gd` | Input-to-movement scene regression |
| `src/tests/fixtures/player_input_profile_335.json` | Turn-rate and selected binding provenance |
## Related decisions and references
+54 -28
View File
@@ -5,24 +5,25 @@
| Field | Value |
|---|---|
| Status | Partial |
| Target/work package | M02 / M02-GMP-INPUT-001, M02-GMP-MOVEMENT-001 and M02-RND-CAMERA-001 consumer updates |
| Target/work package | M02 / M02-GMP-INPUT-001, M02-GMP-SANDBOX-CAPABILITIES-001, M02-GMP-INPUT-PROFILE-335-001 |
| Owners | Gameplay input boundary; `sindo-main-codex` for current package |
| Last verified | `8c1cf9b`, 2026-07-14 |
| Profiles/capabilities | Current render-sandbox defaults; `Blizzlike335` binding semantics not yet verified |
| Last verified | M02-GMP-INPUT-PROFILE-335-001 worktree, 2026-07-15 |
| Profiles/capabilities | Distinct RenderSandbox and Blizzlike335 action layouts; debug execution remains RenderSandbox-only |
## Purpose
Convert remappable Godot Input Map actions into an immutable, scene-free
`MoveIntent` that can be consumed by the current player controller and a later
independently testable local movement controller.
`MoveIntent` that can be consumed by the independently testable local movement
controller. Select either the preserved renderer-sandbox layout or the verified
build-12340 movement bindings at the scene composition boundary.
## Non-goals
- Define authoritative server movement, prediction or reconciliation.
- Own character transform, terrain queries, camera state or animation state.
- Claim that the current W/S/A/D sandbox mapping exactly matches build 12340.
- Make sprint or free flight available to the production compatibility profile;
their profile gate remains a later M02 package.
- Implement jump, autorun, left-button select/move or persisted binding settings.
- Remove process-wide debug actions; Blizzlike335 sampling ignores them and the
movement capability boundary also rejects directly constructed debug requests.
## Context and boundaries
@@ -30,8 +31,10 @@ independently testable local movement controller.
flowchart LR
Devices[Keyboard and mouse] --> InputMap[Godot Input Map]
InputMap --> Source[PlayerInputSource]
Profile[RenderSandbox or Blizzlike335] --> Source
Source --> Intent[MoveIntent]
Intent --> Movement[LocalPlayerMovementController]
Profile[PlayerMovementCapabilities] --> Movement
Movement --> Controller[ThirdPersonWowController adapter]
Controller --> Camera[ThirdPersonCameraRig]
```
@@ -52,11 +55,13 @@ Forbidden dependencies:
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `MoveIntent` | Immutable value | Carries normalized forward, strafe and vertical axes plus the sandbox sprint request | Created and consumed on the main physics tick; caller retains reference | Constructor clamps axes; no I/O failure |
| `MoveIntent` | Immutable value | Carries normalized forward, strafe, vertical and turn axes plus the sandbox sprint request | Created and consumed on the main physics tick; caller retains reference | Constructor clamps axes; no I/O failure |
| `MoveIntent.has_translation()` | Query | Reports whether any translation axis is non-zero | Any thread while value remains immutable | None |
| `PlayerInputActions` | Constants | Provides stable action names shared by configuration and adapters | Process lifetime | Missing project actions are detected by verification |
| `PlayerInputSource.sample_move_intent()` | Adapter method | Samples configured actions for one physics tick | Main thread; returned intent is independent of adapter lifetime | Missing actions resolve to zero strength and are a configuration defect |
| `PlayerInputSource.new(profile_id)` | Constructor | Selects sandbox or compatibility action layout once | Scene lifetime | Unknown IDs fail closed to `Blizzlike335` |
| `PlayerInputSource.sample_move_intent()` | Adapter method | Samples the selected action layout for one physics tick | Main thread; returned intent is independent of adapter lifetime | Missing actions resolve to zero strength and are a configuration defect |
| `PlayerInputSource.compose_move_intent(...)` | Pure factory | Clamps strengths, cancels opposites and normalizes diagonal planar input | Any thread with scalar inputs | None |
| `PlayerInputSource.compose_blizzlike_335_move_intent(...)` | Pure factory | Composes Q/E strafe, A/D turn and right-mouse A/D strafe semantics | Any thread with scalar inputs | None |
## Inputs and outputs
@@ -65,6 +70,8 @@ Forbidden dependencies:
| Input | Movement action strengths | Godot `InputMap`/`Input` | `PlayerInputSource` | Engine-owned snapshot | Sampled on main physics tick |
| Input | Mouse action events | Godot `_unhandled_input` dispatch | `ThirdPersonCameraRig` through player dispatch | Engine-owned event | Current input dispatch only |
| Output | `MoveIntent` | `PlayerInputSource` | `LocalPlayerMovementController` through scene composition | Immutable caller-held value | One physics tick |
| Output | Debug sprint request/vertical axes | `PlayerInputSource` | Capability-gated movement controller | Immutable intent fields | One physics tick |
| Output | Signed turn axis | `PlayerInputSource` | `LocalPlayerMovementController` | Immutable scalar | One physics tick |
| Output | Camera action names | `PlayerInputActions` | `ThirdPersonCameraRig` | Static constants | Process lifetime |
Side effects:
@@ -81,9 +88,11 @@ flowchart LR
Project[project.godot action defaults] --> Map[InputMap]
Remap[User or editor remapping] --> Map
Map -->|movement strengths| Source[PlayerInputSource]
Profile[Selected input profile] --> Source
Map -->|camera events| Camera[ThirdPersonCameraRig]
Source -->|clamp cancel normalize| Intent[MoveIntent]
Intent --> Consumer[LocalPlayerMovementController]
Profile[PlayerMovementCapabilities] --> Consumer
Consumer --> Adapter[ThirdPersonWowController]
```
@@ -92,7 +101,9 @@ flowchart LR
The adapter is stateless. The scene composition root creates one
`PlayerInputSource` and `LocalPlayerMovementController` in `_ready`; each physics
tick produces a new immutable intent. Flight enabled/disabled state is owned by
the movement controller and is not hidden in the input adapter.
the movement controller and is not hidden in the input adapter. RenderSandbox
retains debug requests in its intent; Blizzlike335 does not sample them, and the
immutable movement capability also rejects directly constructed requests.
## Main sequence
@@ -106,7 +117,7 @@ sequenceDiagram
Engine->>Source: movement action strengths
Source->>Intent: compose normalized axes and requests
Source-->>Player: sample_move_intent()
Player->>Move: advance(intent, selected basis, delta)
Player->>Move: calculate yaw delta, then advance(intent, selected basis, delta)
Move-->>Player: displacement and velocity state
```
@@ -127,6 +138,7 @@ sequenceDiagram
| Conflicting opposite actions | Pure composition | Strengths cancel deterministically | Covered by contract test | Release/remap one action |
| Strength outside range | Pure composition | Value clamps to `[0, 1]` before axis calculation | Covered by contract test | Input adapter may be corrected independently |
| Controller disabled/unloaded | Scene lifecycle | Sampling stops with physics processing | Existing scene lifecycle | Re-enable or recreate scene |
| Debug request in `Blizzlike335` | Input profile and movement capability checks | Input adapter omits it; directly constructed request has no effect | Profile/capability regressions | Use `RenderSandbox` only when debug movement is intended |
There is no asynchronous operation to cancel and no persisted state to roll back.
@@ -134,12 +146,15 @@ There is no asynchronous operation to cancel and no persisted state to roll back
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Forward/backward | Physical W/S | Current render sandbox | Yes, through Input Map | Produces signed forward axis |
| Strafe left/right | Physical A/D | Current render sandbox | Yes | Produces signed right axis |
| Forward/backward | Physical W/S and Up/Down | Both | Yes, through Input Map | Produces signed forward axis |
| Strafe left/right | Physical A/D | `RenderSandbox` | Yes | Preserves renderer baseline |
| Strafe left/right | Physical Q/E | `Blizzlike335` | Yes | Matches selected build-12340 defaults |
| Keyboard turn | Physical A/D and Left/Right | `Blizzlike335` | Yes | Produces signed turn axis |
| Camera-held A/D | Physical A/D while right mouse is held | `Blizzlike335` | Yes | Produces strafe and suppresses keyboard turn |
| Camera rotate | Right mouse button | Current render sandbox | Yes | Captures/releases mouse for orbit |
| Camera zoom | Mouse wheel | Current render sandbox | Yes | Adjusts current camera distance |
| Debug sprint | Shift | Sandbox debug only by intent; profile gate pending | Yes | Requests existing speed multiplier |
| Debug flight up/down/toggle | E/Q/Space | Sandbox debug only by intent; profile gate pending | Yes | Requests existing free-flight controls |
| Debug sprint | Shift | Executable only in `RenderSandbox` | Yes | Requests multiplier; `Blizzlike335` ignores it |
| Debug flight up/down/toggle | E/Q/Space | Executable only in `RenderSandbox` | Yes | Requests free flight; `Blizzlike335` remains grounded |
## Persistence, cache and migration
@@ -157,13 +172,20 @@ provide a settings migration once user settings are persisted.
## Verification
- Unit/contract tests: `src/tools/verify_player_input.gd` validates all actions,
default bindings, axis cancellation/clamping/normalization and debug request flags.
both profile defaults, axis cancellation/clamping/normalization, turn routing,
safe profile fallback, fixture provenance and debug request flags.
- Integration/E2E: `src/tests/scenes/player_input_regression.tscn` is loaded by
the verifier, which drives forward, camera-zoom and immediate sandbox-flight
actions through the real scene controller and also rejects direct
physical-key polling.
- Fidelity evidence: current sandbox defaults retain W/S/A/D, E/Q, Shift, Space,
right mouse and wheel bindings. This is observable-regression coverage only.
the verifier, which drives sandbox forward/camera/flight plus compatibility
keyboard turn and right-mouse strafe through real scene controllers.
- Profile integration: `verify_player_movement_capabilities.gd` drives sprint
and flight actions through real `RenderSandbox` and `Blizzlike335` scene instances.
- Fidelity evidence: sanitized fixture `player_input_profile_335.json` pins the
private build-12340 `DefaultBindings.wtf`/`Bindings.xml` hashes and selected
movement/camera commands. TrinityCore 3.3.5 revision
`2853a621d6af91a803787a2b8a509f4ce3e0300d` pins the server-contract player
turn rate. WoWee revision `626243e937fb93965fa583a6507ed5a1aa7dda4b`
supports right-mouse A/D strafe routing. Both are compatibility references,
not direct original-client timing/conditional-behavior proof.
- Performance budgets: no I/O, jobs or scene lookup per sample; one immutable
intent allocation per physics tick.
- Manual diagnostics: run the Eastern Kingdoms scene and remap one movement
@@ -184,15 +206,15 @@ provide a settings migration once user settings are persisted.
|---|---|---|---|
| Remappable current sandbox controls | Implemented | `verify_player_input.gd` action/default checks | Add persisted user binding settings later |
| `PlayerInputSource → MoveIntent` seam | Implemented | Pure composition and controller boundary checks | Local movement consumer is now extracted |
| Exact 3.3.5a default semantics | Unknown | No original-client binding fixture in this package | Capture build-12340 defaults and mouse-turn/strafe policy |
| Production profile exclusion of sprint/flight | Partial | Debug actions are named explicitly | Add typed profile/capability gate in M02 |
| Selected 3.3.5a movement defaults | Implemented | Pinned private-source hashes, sanitized selected bindings and profile regressions | Jump, autorun and left-button behavior remain planned |
| Compatibility-profile exclusion of sprint/flight | Implemented | Typed capability and two real-scene regressions | Application shell selection remains future work |
| Jump/fall/swim/fly gameplay semantics | Planned | M02 target and gameplay roadmap | Implement after terrain/movement state extraction |
## Known gaps and risks
- The defaults preserve the pre-M02 sandbox behavior, not verified original-client semantics.
- Sprint and free flight are still executable by the sandbox controller until a
typed build-profile gate is introduced.
- Only selected movement/camera binding facts are implemented; the fixture explicitly records unimplemented defaults.
- Input Map still exposes debug bindings process-wide; the movement consumer now
rejects their effects outside `RenderSandbox`.
- Camera state is extracted; active collision remains an explicit planned policy.
- There is no persisted user keybinding format or UI yet.
@@ -205,13 +227,17 @@ provide a settings migration once user settings are persisted.
| `src/gameplay/input/player_input_actions.gd` | Stable project action names |
| `src/gameplay/input/player_input_source.gd` | Engine Input Map adapter and pure composition |
| `src/gameplay/movement/local_player_movement_controller.gd` | Scene-free movement consumer |
| `src/gameplay/movement/player_movement_capabilities.gd` | Defense-in-depth execution gate for debug requests |
| `src/scenes/player/third_person_camera_rig.gd` | Camera-action consumer and orbit/zoom state |
| `src/scenes/player/third_person_wow_controller.gd` | Composition and input-event dispatcher |
| `src/tests/scenes/player_input_regression.tscn` | Asset-free controller movement/camera regression fixture |
| `src/tests/fixtures/player_input_profile_335.json` | Sanitized build-12340 binding and server turn-rate provenance |
| `src/tools/verify_player_input.gd` | Headless contract/integration regression |
| `src/tools/verify_player_movement_capabilities.gd` | Sandbox versus compatibility-profile integration regression |
## Related decisions and references
- ADR: none; this implements the existing architecture and M02 seam.
- Upstream/reference: `targets/02-player-decomposition.md`,
`targets/roadmap/04-gameplay-systems.md`, `docs/ARCHITECTURE.md`.
`targets/roadmap/04-gameplay-systems.md`, `docs/ARCHITECTURE.md`, and the
pinned sources recorded in `player_input_profile_335.json`.
+216
View File
@@ -0,0 +1,216 @@
# Render Budget Scheduler
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | `M03-RND-SCHEDULER-001` |
| Owners | Renderer workstream / M03 integrator |
| Last verified | Worktree `work/sindo-main-codex/m03-render-budget-scheduler`, 2026-07-16 |
| Profiles/capabilities | Runtime and Editor main-thread operation quotas; profile values supplied by streamer composition |
## Purpose
Issue a bounded number of permits for each renderer operation lane during one
process frame. The scheduler centralizes quota accounting and teardown
cancellation without taking ownership of queues or render work.
## Non-goals
- Own, sort, drain or cap the stored length of a renderer queue.
- Submit, cancel or await worker and `ResourceLoader` tasks.
- Execute callbacks or mutate the SceneTree, resources, caches or RIDs.
- Adapt limits from elapsed time or create a general job/dependency framework.
- Select renderer quality or compatibility profiles.
## Context and boundaries
```mermaid
flowchart LR
Config[Streamer exported per-frame limits] --> Frame[operation limit snapshot]
Frame --> Scheduler[RenderBudgetScheduler]
Queues[StreamingWorldLoader owned queues] --> Drains[Ordered queue drains]
Scheduler -->|boolean permits| Drains
Drains --> Render[Main-thread finalize/attach/evict]
Shutdown[StreamingWorldLoader exit] -->|cancel| Scheduler
Scheduler -. does not own .-> Queues
```
Allowed dependencies are Godot value containers and `RefCounted`. The module
must not depend on `Node`, `WorkerThreadPool`, `ResourceLoader`, `RenderingServer`,
mutexes, renderer assets or streamer queue fields.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Failure behavior |
|---|---|---|---|---|
| Lane constants | `StringName` constants | Stable operation categories used in the frame limit map | Process lifetime | Unknown lanes have zero permits |
| `begin_frame` | Method | Replaces all limits and clears consumption diagnostics for one frame | Main thread; once before drains | Negative limits clamp to zero; cannot revive a cancelled instance |
| `has_remaining_permit` | Query | Tests a lane without consuming | Main thread; current frame | Unknown/exhausted/cancelled returns `false` |
| `try_consume_permit` | Mutation | Atomically checks and consumes one caller operation permit | Main thread; current frame | Unknown/exhausted/cancelled returns `false` |
| `remaining_permits` | Query | Returns a lane's current unconsumed count | Main thread; current frame | Unknown/cancelled returns zero |
| `consumed_permits_snapshot` | Query | Returns detached per-lane diagnostics | Caller owns returned dictionary | Mutation cannot affect scheduler state |
| `cancel` | Lifecycle method | Permanently stops new permits and clears remaining limits | Main thread; renderer teardown | Idempotent; does not interrupt in-flight caller work |
| `is_cancelled` | Query | Reports terminal lifecycle state | Scheduler lifetime | No failure |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | `Dictionary[StringName, int]` operation limits | `StreamingWorldLoader` configuration snapshot | Scheduler | Copied by scheduler | One process frame |
| Input | Lane ID | Ordered streamer drain | Scheduler | Constant value | One permit check |
| Input | Cancellation | Streamer `_exit_tree` | Scheduler | Terminal flag | Renderer instance lifetime |
| Output | Permit boolean | Scheduler | Streamer drain loop | Scalar | One attempted operation |
| Output | Remaining count | Scheduler | Streamer/tests | Scalar | Current frame |
| Output | Consumed snapshot | Scheduler | Diagnostics/tests | Detached caller-owned copy | Current frame snapshot |
The scheduler has no I/O, logging, SceneTree, task, cache or GPU side effects.
## Data flow
```mermaid
flowchart TD
Exports[Exported operation limits] --> Limits[_render_operation_limits_for_frame]
Limits --> Begin[begin_frame]
Begin --> Remaining[remaining permits by lane]
Queue[Non-empty caller-owned queue] --> Check[try_consume_permit]
Remaining --> Check
Check -->|true| Execute[Caller performs one operation]
Check -->|false| Defer[Operation remains queued]
Execute --> Consumed[consumed permits diagnostics]
```
## Lifecycle and state
```mermaid
stateDiagram-v2
[*] --> Ready
Ready --> FrameActive: begin_frame
FrameActive --> FrameActive: consume/check
FrameActive --> FrameActive: begin_frame resets limits
Ready --> Cancelled: cancel
FrameActive --> Cancelled: cancel
Cancelled --> Cancelled: begin_frame/check/cancel
Cancelled --> [*]
```
Cancellation is intentionally terminal. A new renderer scene creates a new
scheduler instance; an old instance cannot accidentally resume after teardown.
## Main sequence
```mermaid
sequenceDiagram
participant Stream as StreamingWorldLoader
participant Budget as RenderBudgetScheduler
participant Queue as Loader-owned queues
participant Render as SceneTree/RenderingServer
Stream->>Budget: begin_frame(operation limits)
loop existing fixed drain order
Stream->>Queue: inspect next operation
Stream->>Budget: try_consume_permit(lane)
Budget-->>Stream: true or false
Stream->>Render: execute one operation when true
end
Stream->>Budget: cancel() during _exit_tree
Budget-->>Stream: later permits rejected
Stream->>Stream: await tasks and release owned resources
```
## Ownership, threading and resources
- `StreamingWorldLoader` owns every queue, task registry, cache, Node, Resource and RID.
- The scheduler owns two small per-frame counter dictionaries and one terminal flag.
- The streamer preserves operation order. In particular, chunk removals and
creations share `CHUNK_GEOMETRY`, with removals drained first.
- All current scheduler calls occur on the Godot main thread. The service has no
mutex and is not safe for concurrent mutation.
- A scheduler instance lives exactly as long as its loader instance.
## Errors, cancellation and recovery
| Condition | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Negative/zero limit | `begin_frame` clamp | Lane issues no permits | Contract verifier | Correct configuration or accept disabled lane |
| Unknown lane | Permit lookup miss | Returns `false`/zero | Contract verifier | Add the lane to the frame snapshot |
| Exhausted lane | Remaining count is zero | Work remains in caller queue for a later frame | Queue-depth/hitch metrics | Next `begin_frame` replenishes permits |
| Renderer teardown | Streamer calls `cancel` before task waits | New permits stop immediately | Shutdown ownership regression | New scene creates a new scheduler |
| In-flight operation at cancel | Caller already consumed permit | Scheduler does not interrupt it | Ownership docs | Caller completes/cleans it through existing shutdown path |
This cancellation scope is permit issuance, not worker cancellation. Existing
task/result staleness and shutdown waits remain streamer responsibilities.
## Configuration and capabilities
The streamer's existing exported values populate 16 lanes: tile finalization,
terrain upgrade/control-splat/splat-cache/splat-build, water finalization, shared
chunk geometry, tile-load starts, tile LOD create/remove, M2 animation/mesh
finalization and build, WMO instance/group build, and detail synchronization.
Defaults and quality-preset assignments are unchanged. `tile_finalize` retains
its historical minimum of one; other negative values behave as zero.
## Persistence, cache and migration
The module is runtime-only and serializes nothing. It changes no cache, asset,
scene, protocol or database format and requires no version bump or migration.
## Diagnostics and observability
- `consumed_permits_snapshot` exposes detached per-frame counts for tests and
future facade metrics without exposing mutable scheduler state.
- Existing queue depths, named hitch sections and `HITCH`/`PERF` logs remain in
`StreamingWorldLoader`.
- No scheduler logging occurs on the hot permit path.
## Verification
- `src/tools/verify_render_budget_scheduler.gd` covers exact exhaustion,
independent lanes, shared removal/create priority, frame reset, invalid/unknown
lanes, detached diagnostics, terminal cancellation and source boundaries.
- The verifier performs 20,000 permit checks with a generous 150 ms regression
ceiling on the headless runner; this is a guard, not a renderer p95 claim.
- Integration regressions must retain the seven renderer checkpoint plans and
existing focus/planner/facade contracts.
- Fidelity evidence is behavior-preserving: exported values, process order,
cache versions and visual operations are unchanged. No build-12340 parity
claim follows from this extraction.
## Extension points
- Add facade consumption metrics using detached snapshots.
- Move a service's queue drain behind its own boundary while retaining these lane IDs.
- Add measured time-aware policy only after asset-backed p95/p99 evidence proves
fixed operation counts insufficient; do not embed callbacks in this scheduler.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Per-lane frame bounds | Implemented | Exact exhaustion and independent-lane contracts | Asset-backed p95/p99 baseline remains |
| Shared removal/create priority | Implemented | Synthetic shared `CHUNK_GEOMETRY` contract | Other service priorities remain streamer-owned |
| Teardown permit cancellation | Implemented | Terminal cancellation contract and loader source gate | Worker stale-result cancellation remains streamer-owned |
| Detached consumption diagnostics | Implemented | Snapshot isolation contract | Not yet exposed through facade metrics |
## Known gaps and risks
- Queue storage length is not capped; only per-frame draining is bounded.
- Worker concurrency and stale-result cancellation remain existing streamer logic.
- Exact p95/p99 impact requires local extracted assets and paired baseline runs.
- Lane IDs are dynamic dictionary keys; the verifier guards the current contract.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/streaming/render_budget_scheduler.gd` | Scene-free lane counters, permits, diagnostics and cancellation |
| `src/scenes/streaming/streaming_world_loader.gd` | Limit composition, fixed drain order, queue/resource ownership and operation execution |
| `src/tools/verify_render_budget_scheduler.gd` | Asset-free behavior, dependency and bounded timing regression |
## Related decisions and references
- [`world-renderer.md`](world-renderer.md)
- [`streaming-target-planner.md`](streaming-target-planner.md)
- [`../../targets/03-renderer-facade.md`](../../targets/03-renderer-facade.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
- [`../CODING_STANDARD.md`](../CODING_STANDARD.md)
+239
View File
@@ -0,0 +1,239 @@
# Streaming Target Planner
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | `M03-RND-STREAMING-PLANNER-001` |
| Owners | Renderer workstream / M03 integrator |
| Last verified | `e887c3b`, 2026-07-15 |
| Profiles/capabilities | Runtime and Editor preview ADT target selection; profile values supplied by caller |
## Purpose
Calculate which available ADT tiles are wanted and retained for one typed focus
without accessing the SceneTree, worker tasks, renderer queues, caches or GPU
resources. Runtime planning preserves prewarm, hysteresis and boundary-prefetch
behavior; editor planning preserves the square preview radius.
## Non-goals
- Mutate load/finalize/eviction queues.
- Prioritize queued tile paths by world distance.
- Calculate chunk or tile LOD levels after a tile is loaded.
- Own the available WDT tile catalog or cache paths.
- Create, cancel or await jobs.
- Implement M2, WMO, liquid or terrain attachment.
## Context and boundaries
```mermaid
flowchart LR
Focus[GodotWorldPosition] --> Planner[StreamingTargetPlanner]
Policy[StreamingTargetPolicy] --> Planner
Catalog[Available tile keys from WDT/directory] --> Planner
Planner --> Plan[StreamingTargetPlan]
Plan --> Streamer[StreamingWorldLoader queue/state apply]
Streamer --> Queue[Tile load queue]
```
Allowed dependencies:
- typed coordinate values and `CoordinateMapper`;
- immutable planner policy/result contracts;
- read-only available-tile key membership.
Forbidden dependencies:
- `Node`, SceneTree and viewport/camera discovery;
- `WorkerThreadPool`, `ResourceLoader` and filesystem access;
- `RenderingServer`, meshes, materials, nodes and RIDs;
- gameplay, network, UI, editor UI or server adapters;
- streamer queues, tile states and cache dictionaries.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `StreamingTargetPlanner.plan_runtime` | Pure method | Builds wanted/retained sets with edge/corner prefetch | Any thread if caller does not mutate catalog concurrently; call-scoped | Empty catalog produces empty sets |
| `StreamingTargetPlanner.plan_editor` | Pure method | Builds identical editor wanted/retained square sets | Same as runtime planning | Negative radius clamps to zero |
| `StreamingTargetPolicy` | Immutable value | Captures radius margins and boundary threshold for one plan | Caller-owned, shareable | Values retain scene input; effective margins/threshold are clamped |
| `visible_tile_radius` | Pure query | Resolves chunk-radius versus tile-radius visibility | Any thread/value lifetime | No failure |
| `load_tile_radius` | Pure query | Adds non-negative prewarm margin | Any thread/value lifetime | No failure |
| `retained_tile_radius` | Pure query | Adds non-negative hysteresis margin | Any thread/value lifetime | No failure |
| `StreamingTargetPlan` | Immutable value | Holds focus tile and read-only wanted/retained key sets | Caller-owned plan lifetime | No failure |
| `wanted_tile_keys` | Read-only query | Returns immutable wanted set | Plan lifetime | Mutation is rejected by Godot read-only Dictionary |
| `retained_tile_keys` | Read-only query | Returns immutable retained set | Plan lifetime | Mutation is rejected by Godot read-only Dictionary |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | `GodotWorldPosition` | `WorldRenderFacade`/streamer focus adapter | Planner | Immutable caller value | One plan |
| Input | Available `<tile_x>_<tile_y>` keys | `StreamingWorldLoader` WDT/directory catalog | Planner membership checks | Loader-owned, read-only during call | Map session |
| Input | `StreamingTargetPolicy` | Streamer configuration snapshot | Planner | Immutable caller value | One or more plans |
| Input | Editor preview radius | Editor scene configuration | Planner | Scalar copy | One plan |
| Output | `StreamingTargetPlan.focus_tile` | Planner/`CoordinateMapper` | Streamer diagnostics | Immutable typed coordinate | Plan lifetime |
| Output | Wanted tile-key set | Planner | Streamer queue/state apply | Plan-owned read-only Dictionary | Plan lifetime |
| Output | Retained tile-key set | Planner | Streamer hysteresis/state apply | Plan-owned read-only Dictionary | Plan lifetime |
Side effects:
- none;
- no input mutation;
- no logging, I/O, task submission, cache access, SceneTree or GPU work.
## Data flow
```mermaid
flowchart TD
Position[GodotWorldPosition] --> Tile[CoordinateMapper.godot_to_adt_tile]
Position --> Local[CoordinateMapper.godot_to_adt_tile_local]
Policy[StreamingTargetPolicy] --> Radii[load and retained radii]
Policy --> Threshold[clamped boundary threshold]
Tile --> Centers[base prefetch center]
Local --> Centers
Threshold --> Centers
Centers --> Wanted[available keys within load radius]
Centers --> Retained[available keys within retained radius]
Catalog[Available tile keys] --> Wanted
Catalog --> Retained
Wanted --> Plan[StreamingTargetPlan]
Retained --> Plan
Tile --> Plan
```
## Lifecycle/state
The planner is stateless. A call creates local center/set dictionaries, freezes
the result dictionaries in `StreamingTargetPlan`, returns the plan and retains
no reference to inputs or output. There is no retry, cancellation or shutdown
state machine.
## Main sequence
```mermaid
sequenceDiagram
participant Stream as StreamingWorldLoader
participant Planner as StreamingTargetPlanner
participant Mapper as CoordinateMapper
participant Plan as StreamingTargetPlan
Stream->>Planner: plan_runtime(catalog, typed focus, policy)
Planner->>Mapper: focus tile and tile-local position
Mapper-->>Planner: typed coordinates
Planner->>Planner: prefetch centers and available key sets
Planner->>Plan: freeze focus/wanted/retained result
Plan-->>Stream: immutable target plan
Stream->>Stream: apply queues, LOD and detail state
```
## Ownership, threading and resources
- `StreamingWorldLoader` owns the available catalog and must not mutate it during a planning call.
- `StreamingTargetPolicy` and typed focus remain caller-owned immutable values.
- The planner owns temporary center/set values only until plan construction.
- `StreamingTargetPlan` owns its read-only result dictionaries.
- The planner creates no Node, Resource, RID, thread, mutex or task.
- Current runtime calls occur on the main thread; scene-free contracts permit a
future worker call only after catalog publication/ownership is explicit.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty/unavailable catalog | No membership matches | Valid plan with empty sets | Streamer retains existing missing-data diagnostics | Load a valid WDT/directory catalog and replan |
| Focus outside catalog/grid | Typed coordinate does not match keys | Valid focus tile with filtered empty/partial sets | Caller diagnostic if required | Supply valid focus/map or catalog |
| Negative prewarm/retain margin | Policy effective-radius query | Margin clamps to zero | Contract test | Correct configuration if negative value was unintended |
| Boundary threshold outside range | Policy query | Clamp to `0.0..0.49` | Contract test | Correct configuration or accept safe clamp |
| Plan no longer current | New focus/config/catalog revision | Caller discards ephemeral plan | Streamer refresh diagnostics | Replan from current inputs |
Cancellation is not applicable: planning is synchronous, bounded and owns no
external work. A returned stale plan is simply not retained by the planner.
## Configuration and capabilities
| Setting/capability | Default source | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| `lod2_radius_chunks` | Streamer scene/profile | Renderer | Yes at composition/debug boundary | Converts chunk coverage to minimum ADT radius |
| `lod2_tile_radius` | Streamer scene/profile | Renderer | Yes | Minimum visible ADT radius |
| `warm_tile_margin` | Streamer scene/profile | Renderer | Yes | Extends chunk-derived visible radius |
| `prewarm_tile_margin` | Streamer scene/profile | Renderer | Yes | Extends wanted/load radius |
| `retain_tile_margin` | Streamer scene/profile | Renderer | Yes | Extends retained hysteresis radius |
| `boundary_prefetch_threshold` | Streamer scene/profile | Renderer | Yes | Adds adjacent/corner prefetch centers near tile boundaries |
| Editor preview radius | Editor scene | Authoring preview | Yes | Square wanted/retained preview set |
The planner does not select `Blizzlike335` or `Enhanced`; it applies the values
already selected by renderer composition.
## Persistence, cache and migration
- Planner contracts are runtime-only and are not serialized.
- Existing tile keys and cache paths are unchanged.
- No cache/resource/schema version bump or migration is required.
## Diagnostics and observability
- Logs remain emitted by `StreamingWorldLoader` after applying a plan.
- `StreamingTargetPlan` exposes focus/wanted/retained data for deterministic tests.
- Dedicated verifier reports case count, iteration count, total time and average plan time.
- No correlation ID is required for synchronous ephemeral plans.
## Verification
- Unit/contract: `src/tools/verify_streaming_target_planner.gd` covers center
radii, edge/corner cross product, catalog filtering, clamps, editor behavior,
input/result ownership and scene-free dependencies.
- Integration: streamer source gate confirms both runtime/editor delegation and
removal of `_predictive_focus_tiles` from the monolith.
- Fidelity evidence: expected sets encode the pre-extraction formulas; renderer
checkpoint dry-run must retain all seven checkpoint plans.
- Performance budget: 250 synthetic High-like corner plans average at most
`4.0 ms` each on the headless regression runner; measured result is recorded
in test output.
- Manual diagnostics: unchanged `StreamingWorld` focus/wanted/queued log.
## Extension points
- Direction/velocity/frustum priority can extend policy/input only with fixtures
and a compatibility-safe default reproducing the current position-only plan.
- Multi-focus planning may union additional typed centers without giving the
planner ownership of queues or consumers.
- Queue ordering remains a separate scheduler/application responsibility.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Runtime wanted/retained sets | Implemented | Center/corner/catalog fixtures | Add paired traversal performance evidence |
| Boundary prefetch centers | Implemented | Edge/corner cross-product fixture | Direction/velocity prediction not implemented |
| Editor preview set | Implemented | Negative/radius-one fixtures | Editor viewport integration remains existing adapter |
| Scene-free deterministic execution | Implemented | Dependency/source gate and 250-iteration run | Worker execution is unnecessary today |
| Queue priority/LOD | Not in module | Preserved in streamer | Separate extraction packages |
## Known gaps and risks
- Tile-key sets preserve the streamer's internal string representation; a typed
catalog migration is deferred until it can remove, rather than duplicate, that source of truth.
- Planner time scales with the union of square radii/prefetch centers. The
current bounded regression covers High-like corner input but not pathological custom radii.
- Exact p95/p99 frame impact still requires renderer baseline runs with local assets.
- Direction, velocity and camera frustum do not influence target selection yet.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/streaming/streaming_target_planner.gd` | Pure runtime/editor set calculation and boundary centers |
| `src/render/streaming/streaming_target_policy.gd` | Immutable radius/threshold snapshot and effective clamps |
| `src/render/streaming/streaming_target_plan.gd` | Immutable typed focus and read-only result sets |
| `src/scenes/streaming/streaming_world_loader.gd` | Planner composition and plan application to queues/state |
| `src/tools/verify_streaming_target_planner.gd` | Asset-free behavior/dependency/performance regression |
## Related decisions and references
- [`world-renderer.md`](world-renderer.md)
- [`../../targets/03-renderer-facade.md`](../../targets/03-renderer-facade.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
- [`../GODOT_BEST_PRACTICES.md`](../GODOT_BEST_PRACTICES.md)
- [`../CODING_STANDARD.md`](../CODING_STANDARD.md)
+6 -2
View File
@@ -5,9 +5,9 @@
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M02 / M02-RND-CAMERA-001 |
| Target/work package | M02 / M02-RND-CAMERA-001 and M02-GMP-INPUT-PROFILE-335-001 consumer update |
| Owners | Camera presentation state and CameraPivot/Camera3D lifecycle |
| Last verified | `8c1cf9b`, 2026-07-14 |
| Last verified | M02-GMP-INPUT-PROFILE-335-001 worktree, 2026-07-15 |
| Profiles/capabilities | Current sandbox orbit/zoom; identity collision policy |
## Purpose
@@ -60,6 +60,7 @@ Forbidden dependencies:
|---|---|---|---|---|
| `ThirdPersonCameraRig` | `Node3D` component | Owns orbit/zoom state on CameraPivot | Main thread; scene-owned | Reports missing Camera3D |
| `initialize_for_character(character_body)` | Composition method | Attaches yaw target and captures initial character yaw | Main thread after scene ready | Null leaves yaw target absent |
| `synchronize_yaw_from_character()` | Composition method | Refreshes orbit yaw after keyboard movement turns the character | Main physics thread | Missing character leaves yaw unchanged |
| `handle_camera_input(event)` | Input adapter | Applies capture/release/zoom/orbit actions | Main input thread | Returns whether camera consumed state |
| `set_collision_policy(policy)` | Composition method | Replaces distance resolution policy | Main thread | Null rejected; old policy retained |
| `refresh_camera_transform()` | Presentation update | Applies pivot pose and resolved camera distance | Main/physics thread | Missing camera leaves state but no Camera3D mutation |
@@ -77,6 +78,7 @@ Read-only rig state: `yaw_radians`, `pitch_radians`,
| Input | Camera action `InputEvent` | Godot/player dispatch | Camera rig | Engine-owned event | One dispatch |
| Input | Mouse relative pixels | Godot input | Camera rig | Engine-owned event | One dispatch |
| Input | Character `Node3D` | Player composition | Camera rig | Scene-owned reference | Rig scene lifetime |
| Input | Keyboard-turn synchronization | Player scene after yaw application | Camera rig | Synchronous notification | Current physics tick |
| Input | Collision policy | Composition/test | Camera rig | Rig-held RefCounted | Until replacement |
| Output | Character yaw | Camera rig | Player transform | Scene mutation | Orbit event |
| Output | Pivot pitch/height | Camera rig | CameraPivot | Scene mutation | Refresh |
@@ -194,6 +196,8 @@ tools require no path migration.
- State regression: initial yaw/pitch/distance/height/far plane, capture/release,
mouse orbit, character yaw, pitch clamps and zoom clamps.
- Input-profile integration verifies keyboard yaw synchronization, preventing a
later right-mouse orbit from restoring stale character yaw.
- Policy regression: fixed-distance test policy changes Camera3D Z; identity
policy restores requested distance; baseline policy contains no physics query.
- Scene integration: Eastern Kingdoms, Kalimdor and asset-free regression scenes
+86 -27
View File
@@ -5,9 +5,9 @@
| Field | Value |
|---|---|
| Status | Partial |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; декомпозиция M02–M03 |
| 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` |
| Owners | Renderer workstream / milestone integrator |
| Last verified | Worktree `work/sindo-main-codex/m01-server-spawn-renderer`, 2026-07-13 |
| Last verified | Worktree `work/sindo-main-codex/m03-render-budget-scheduler`, 2026-07-16 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose
@@ -32,7 +32,12 @@ flowchart LR
SpawnFixture[Pinned server spawn fixture] --> SpawnManifest[Spawn render manifest]
SpawnManifest --> Capture
Adapter --> Focus
Focus --> Loader[StreamingWorldLoader]
Focus --> Facade[WorldRenderFacade]
Facade --> Loader[StreamingWorldLoader internal]
Loader --> Planner[StreamingTargetPlanner]
Planner --> TargetPlan[StreamingTargetPlan]
TargetPlan --> Loader
Loader --> Budget[RenderBudgetScheduler]
Assets[Extracted WDT/ADT/M2/WMO/BLP] --> Native[Native loaders]
Cache[Baked terrain/M2/WMO caches] --> Loader
Native --> Parsed[Parsed tile/model data]
@@ -60,36 +65,47 @@ Forbidden dependencies:
## Public API
Текущая система ещё не имеет стабильного facade. Фактический integration surface — `StreamingWorldLoader` Node3D и exported properties. M03 должен заменить этот surface на `WorldRenderFacade`.
Runtime, capture and probe callers use `WorldRenderFacade` for focus and metrics.
`StreamingWorldLoader` remains the internal scene implementation while later M03
packages add environment, entity-visual and ground-query contracts.
Gameplay modules and Godot `EditorPlugin` package sources are repository-gated
from externally reading/writing loader-private queue, task, cache and tile-state fields.
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `map_name` | Exported property | WDT/map directory name | Set before world load | Missing WDT produces diagnostic/empty world |
| `extracted_dir` | Exported property | Root of legally extracted client data | Session lifetime | Missing files reported per loader path |
| `StreamingFocus` | Immutable value | Typed Godot-basis position without Node/Camera dependency | Any thread; caller-owned reference | Null position is a caller contract violation |
| `streaming_focus_source_path` | Exported adapter property | Samples any explicit `Node3D` source into `StreamingFocus` | Main thread, scene lifetime | Missing/wrong node warns once and retains prior focus |
| `set_streaming_focus` | Public method | Accepts focus from app/editor/tool composition | Main thread/session | Null focus is ignored by refresh until replaced |
| `refresh_streaming_focus` | Public method | Samples configured source and applies refresh thresholds | Main thread/session | Returns `false` when no valid focus exists |
| `WorldRenderFacade.streaming_focus_source_path` | Exported adapter property | Samples any explicit `Node3D` source into `StreamingFocus` | Main thread, scene lifetime | Missing/wrong node warns once and retains prior focus |
| `WorldRenderFacade.set_streaming_focus` | Public method | Accepts focus from app/editor/tool composition | Main thread/session | Missing internal renderer is diagnosed; no state is duplicated |
| `WorldRenderFacade.refresh_streaming_focus` | Public method | Samples configured source and delegates existing refresh thresholds | Main thread/session | Returns `false` when dependency/focus is unavailable |
| `WorldRenderFacade.renderer_metrics_snapshot` | Public method | Returns a deep-detached queue/cache/activity snapshot | Main thread/caller-owned result | Missing/invalid implementation result returns empty snapshot with diagnostic |
| `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 |
| `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 |
| `hitch_profiler_enabled` | Exported property | Enables named hitch sections | Runtime mutable | Logging overhead only |
Публичным contract не считаются внутренние dictionaries, queues, job records и generated node names.
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
внутренние dictionaries, queues, job records и generated node names. Scene-owned
loader configuration remains transitional composition data, not a caller API.
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | `StreamingFocus` | Player/spectator/editor/capture adapter | `StreamingWorldLoader` | Immutable caller value | Main thread/session |
| Input | Focus-source `NodePath` | Runtime/test scene composition | Loader source adapter | Scene-owned node, sampled position | Main thread/scene lifetime |
| Input | `StreamingFocus` | Player/spectator/editor/capture adapter | `WorldRenderFacade` | Immutable caller value | Main thread/session |
| Input | Focus-source `NodePath` | Runtime/test scene composition | `WorldRenderFacade` source adapter | Scene-owned node, sampled position | Main thread/scene lifetime |
| 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 |
| Test input | Server-spawn render manifest | Coordinate fixture/verifier | Checkpoint capture tool | Repository-owned diagnostic data | Test-process lifetime |
| Output | Desired tile/detail operations | Streaming planner inside loader | Finalize queues | Loader-owned | Cross-frame |
| Output | `StreamingTargetPlan` | `StreamingTargetPlanner` | Streamer queue/state apply | Immutable ephemeral value | One refresh |
| Internal control | Per-frame lane limits and boolean permits | Streamer configuration / `RenderBudgetScheduler` | Ordered streamer drains | Scheduler-owned counters | Main thread/frame |
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
| Output | Metrics/diagnostics | Hitch/stat instrumentation | Logs/baseline tools | Immutable records | Periodic/frame |
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
| 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 |
Side effects:
@@ -104,7 +120,7 @@ Side effects:
```mermaid
flowchart TD
P[Player/spectator Node3D] --> A[Explicit scene adapter]
PlayerSource[Player/spectator Node3D] --> A[Explicit scene adapter]
C[Capture camera] --> A
SF[Pinned server spawn] --> SM[Validated spawn manifest]
SM --> C
@@ -112,12 +128,14 @@ flowchart TD
E[Editor viewport] --> EA[Editor adapter]
A --> F[StreamingFocus]
EA --> F
F --> T[Compute desired ADT tiles]
WDT[WDT tile catalog] --> T
F --> RF[WorldRenderFacade]
RF --> Planner[StreamingTargetPlanner]
Planner --> T[StreamingTargetPlan apply]
WDT[WDT tile catalog] --> Planner
T --> Q[Tile load queue]
Q --> P[Worker parse/cache load]
P --> R[Result queues]
R --> B[Per-frame budget scheduler]
Q --> Parse[Worker parse/cache load]
Parse --> R[Result queues]
R --> B[RenderBudgetScheduler permits]
B --> Terrain[Terrain attach/upgrade]
B --> M2[M2 group/MultiMesh attach]
B --> WMO[WMO instance/group attach]
@@ -127,8 +145,8 @@ flowchart TD
WMO --> World
Liquid --> World
Marker --> World
F --> E[Eviction/retention decisions]
E --> World
F --> Eviction[Eviction/retention decisions]
Eviction --> World
```
## Lifecycle/state
@@ -155,18 +173,26 @@ stateDiagram-v2
sequenceDiagram
participant Source as Player/Editor/Capture source
participant Focus as StreamingFocus adapter
participant Stream as StreamingWorldLoader
participant Facade as WorldRenderFacade
participant Stream as StreamingWorldLoader internal
participant Planner as StreamingTargetPlanner
participant Worker as Worker task
participant Budget as Main-thread budget
participant Budget as RenderBudgetScheduler
participant Render as SceneTree/RenderingServer
Source->>Focus: sample explicit Godot position
Focus->>Stream: set/refresh typed focus
Stream->>Stream: desired tiles and priorities
Focus->>Facade: set/refresh typed focus
Facade->>Stream: delegate focus and refresh
Stream->>Planner: typed focus, catalog and target policy
Planner-->>Stream: immutable wanted/retained plan
Stream->>Stream: queue priority and loaded-tile LOD/detail state
Stream->>Worker: parse/load tile and detail data
Worker-->>Stream: immutable result
Stream->>Budget: enqueue finalize operations
Budget->>Render: attach bounded terrain/M2/WMO/liquid work
Stream->>Budget: begin_frame(operation limits)
Stream->>Budget: try_consume_permit(lane)
Budget-->>Stream: true while lane remains bounded
Stream->>Render: attach permitted terrain/M2/WMO/liquid work
Stream->>Render: evict outside retention range
Stream->>Budget: shutdown: cancel permit issuance
Stream->>Worker: shutdown: wait for WorkerThreadPool tasks
Stream->>Stream: shutdown: finish registered ResourceLoader requests
Stream->>Render: clear queues and owned tree nodes/RIDs
@@ -176,6 +202,12 @@ sequenceDiagram
## Ownership, threading and resources
- `StreamingWorldLoader` владеет tile states, queues, task registries, active world instances и render caches session scope.
- `WorldRenderFacade` owns only scene-relative adapter paths. It does not own the
streamer, focus source, returned metrics, queues, caches, nodes or RIDs.
- `StreamingTargetPlanner` is stateless and owns only call-local calculations;
its immutable plan is consumed synchronously by the streamer.
- `RenderBudgetScheduler` owns only per-frame lane counters and a terminal
cancellation flag. The streamer retains queue ordering and all operation side effects.
- Focus producers own the immutable `StreamingFocus` reference; the loader retains
the latest reference and converts it to `Vector3` only inside the render boundary.
- Worker tasks не должны менять SceneTree и shared Resource concurrently.
@@ -192,6 +224,8 @@ sequenceDiagram
|---|---|---|---|---|
| Missing WDT/ADT | File/catalog check | Tile/map unavailable | Loader warning/error | Fix extraction/profile, reload |
| Missing focus source | Explicit NodePath resolution | Retain current streamed content and prior valid focus | One warning until source recovers | Fix composition path or call `set_streaming_focus` |
| Missing renderer implementation | Facade path/method validation | Focus/metrics call fails without mutation | One facade diagnostic until recovery | Fix scene composition path/implementation |
| Invalid metrics result | Facade return-type validation | Return empty caller snapshot | Facade error | Restore `Dictionary` implementation contract |
| 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 |
@@ -225,6 +259,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
## Diagnostics and observability
- Logs: `HITCH`, `PERF`, `TERRAIN_QUALITY`, `SKY_LIGHT`, `SKYBOX_MODEL`.
- API snapshot: `WorldRenderFacade.renderer_metrics_snapshot()` returns detached
queue/cache/activity counts without exposing mutable renderer state.
- Metrics: queue depths, active tiles/assets, named finalize times, frame percentiles and max hitch.
- Debug views: render checkpoint scenes/captures; future streaming/LOD/portal overlays.
- Correlation IDs: currently mostly tile/path keys; target architecture adds session/job IDs.
@@ -232,6 +268,14 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
## Verification
- Unit/contract tests: streaming-focus contract/wiring, material mapping, unique-ID dedupe, placement probes, baseline manifest, five-point coordinate calibration, synthetic perceptual checkpoint diff, camera-pose grid plan.
- Pure planner contract: center/corner/catalog/clamp/editor cases plus bounded
High-like iteration timing without loading a world scene.
- Budget scheduler contract: exact lane exhaustion, shared chunk removal/create
priority, independent lanes, frame reset, invalid limits, terminal cancellation,
dependency boundaries and bounded permit timing without loading a world scene.
- Internal-access contract: derives the current streamer's private queue/task/
cache/state field inventory and rejects external member/reflection access from
gameplay or EditorPlugin package code.
- Integration/E2E: Eastern Kingdoms/Kalimdor streaming scenes, seven M00
cold/warm checkpoints and one dedicated mapped server-spawn checkpoint.
- Fidelity evidence: семь локальных build 12340 reference JPG покрывают terrain/ADT/M2/WMO/liquid/sky и реальный `GryphonRoost01`; automated cold/warm metrics имеют `compared=14`, `missing=0`. Sky использует приблизительно paired camera, а animated-M2 остаётся asset-paired/placement-unpaired из-за synthetic probe.
@@ -257,7 +301,10 @@ 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 | Planned | M03 | Current caller uses monolithic Node API |
| Stable renderer facade | Partial | `M03-RND-FACADE-001` contract/runtime/tool regressions | Focus and metrics migrated; environment/entity visuals/ground query 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 internal-access boundary | Implemented | `M03-RND-INTERNAL-ACCESS-GATE-001` scans actual private streamer declarations against gameplay and EditorPlugin sources | Renderer diagnostic probes remain a scoped exception pending ground-query/readiness facade contracts |
## Known gaps and risks
@@ -278,14 +325,26 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
- D3D12 descriptor issues remain. Explicit shutdown ownership removes the GUI capture's Node/resource/RID leaks in focused and seven-checkpoint runs; a timing-dependent ObjectDB warning for anonymous zero-reference `RefCounted` objects remains as a separate engine-teardown diagnostic.
- M2/WMO/material/particle/ribbon/portal parity incomplete.
- Public API is mostly exported configuration rather than stable contracts.
- `probe_render_terrain_height.gd` remains a renderer-only diagnostic exception
that reads tile readiness/terrain internals; it is excluded from the gameplay/
EditorPlugin gate until equivalent ground-query and readiness facade contracts exist.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/world_render_facade.gd` | Stable caller boundary for typed focus and detached renderer metrics |
| `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_plan.gd` | Immutable planner result with read-only tile-key sets |
| `src/render/streaming/render_budget_scheduler.gd` | Scene-free per-frame operation permits and terminal cancellation |
| `src/scenes/streaming/streaming_world_loader.gd` | Current runtime orchestration, queues, caches and attachment |
| `src/domain/streaming/streaming_focus.gd` | Immutable camera-independent streaming position contract |
| `src/tools/verify_streaming_focus.gd` | Headless contract, dependency and runtime/tool wiring regression |
| `src/tools/verify_world_render_facade.gd` | Facade delegation, snapshot isolation and consumer wiring 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_renderer_internal_access.gd` | Gameplay/EditorPlugin boundary gate derived from private streamer fields |
| `addons/mpq_extractor/loaders/adt_builder.gd` | Terrain/material/liquid construction |
| `addons/mpq_extractor/loaders/m2_builder.gd` | Static M2 mesh/material construction |
| `addons/mpq_extractor/loaders/m2_native_animated_builder.gd` | Native animated M2 experiment |
+24
View File
@@ -24,11 +24,13 @@ enabled=PackedStringArray("res://addons/mpq_extractor/plugin.cfg")
openwc_player_move_forward={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194320,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
openwc_player_move_backward={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194322,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
openwc_player_strafe_left={
@@ -41,6 +43,28 @@ openwc_player_strafe_right={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
]
}
openwc_player_blizzlike_strafe_left={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":81,"key_label":0,"unicode":113,"location":0,"echo":false,"script":null)
]
}
openwc_player_blizzlike_strafe_right={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
]
}
openwc_player_turn_left={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194319,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
openwc_player_turn_right={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194321,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
openwc_player_debug_fly_up={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
+1
View File
@@ -0,0 +1 @@
# Reference projects are research inputs, not OpenWC runtime resources.
+4
View File
@@ -2,6 +2,10 @@ Reference-only sources and tools.
This directory contains external projects used to compare WoW file-format behavior and editor pipelines while implementing OpenWC. Runtime code should not depend on anything in this directory.
Godot project scanning is disabled for this directory by `.gdignore`. Reference
assets remain available to Git, text search and external tooling, but Godot must
not generate `.import` sidecars inside nested reference repositories.
- `blender-wow-studio-3.4-1.1.0_Experimental/` - Blender add-on sources for M2/WMO import/export reference.
- `wow.export/` - wow.export source tree and Blender OBJ importer reference.
- `noggit-red/` - Noggit RED source tree from `https://gitlab.com/dirtbikercj/noggit-red`, used as a reference for WoW 3.3.5 map editing, ADT/WMO/M2 placement behavior, UID handling and editor workflows.
Submodule reference/open-realm updated: dfe316d3b0...c7ed0545c8
+9 -1
View File
@@ -21,10 +21,16 @@ var is_sprint_requested: bool:
get:
return _is_sprint_requested
## Signed keyboard turn request: left is negative and right is positive.
var turn_axis: float:
get:
return _turn_axis
var _forward_axis: float
var _strafe_axis: float
var _vertical_axis: float
var _is_sprint_requested: bool
var _turn_axis: float
## Creates one frame's movement request without retaining mutable input state.
@@ -32,12 +38,14 @@ func _init(
forward_axis_value: float = 0.0,
strafe_axis_value: float = 0.0,
vertical_axis_value: float = 0.0,
sprint_requested: bool = false
sprint_requested: bool = false,
turn_axis_value: float = 0.0
) -> void:
_forward_axis = clampf(forward_axis_value, -1.0, 1.0)
_strafe_axis = clampf(strafe_axis_value, -1.0, 1.0)
_vertical_axis = clampf(vertical_axis_value, -1.0, 1.0)
_is_sprint_requested = sprint_requested
_turn_axis = clampf(turn_axis_value, -1.0, 1.0)
## Returns true when the intent requests planar or vertical translation.
+10 -2
View File
@@ -2,13 +2,17 @@ class_name PlayerInputActions
extends RefCounted
## Stable Input Map action names consumed by the runtime player input adapter.
## Defaults preserve the current render-sandbox controls; users may remap every
## action through Godot's Input Map without changing gameplay code.
## Separate translation actions preserve the renderer sandbox while allowing
## Blizzlike335 defaults to match build-12340 movement bindings.
const MOVE_FORWARD := &"openwc_player_move_forward"
const MOVE_BACKWARD := &"openwc_player_move_backward"
const STRAFE_LEFT := &"openwc_player_strafe_left"
const STRAFE_RIGHT := &"openwc_player_strafe_right"
const BLIZZLIKE_STRAFE_LEFT := &"openwc_player_blizzlike_strafe_left"
const BLIZZLIKE_STRAFE_RIGHT := &"openwc_player_blizzlike_strafe_right"
const TURN_LEFT := &"openwc_player_turn_left"
const TURN_RIGHT := &"openwc_player_turn_right"
const DEBUG_FLY_UP := &"openwc_player_debug_fly_up"
const DEBUG_FLY_DOWN := &"openwc_player_debug_fly_down"
const DEBUG_SPRINT := &"openwc_player_debug_sprint"
@@ -23,6 +27,10 @@ const REQUIRED_ACTIONS: Array[StringName] = [
MOVE_BACKWARD,
STRAFE_LEFT,
STRAFE_RIGHT,
BLIZZLIKE_STRAFE_LEFT,
BLIZZLIKE_STRAFE_RIGHT,
TURN_LEFT,
TURN_RIGHT,
DEBUG_FLY_UP,
DEBUG_FLY_DOWN,
DEBUG_SPRINT,
+76 -11
View File
@@ -5,21 +5,86 @@ extends RefCounted
## This adapter owns no movement state and may be replaced independently of the
## local movement controller.
const RENDER_SANDBOX_PROFILE_ID := &"RenderSandbox"
const BLIZZLIKE_335_PROFILE_ID := &"Blizzlike335"
## Samples the current Input singleton state for one physics tick.
func sample_move_intent() -> MoveIntent:
return compose_move_intent(
Input.get_action_strength(PlayerInputActions.MOVE_FORWARD),
Input.get_action_strength(PlayerInputActions.MOVE_BACKWARD),
Input.get_action_strength(PlayerInputActions.STRAFE_LEFT),
Input.get_action_strength(PlayerInputActions.STRAFE_RIGHT),
Input.get_action_strength(PlayerInputActions.DEBUG_FLY_UP),
Input.get_action_strength(PlayerInputActions.DEBUG_FLY_DOWN),
Input.is_action_pressed(PlayerInputActions.DEBUG_SPRINT)
var input_profile_id: StringName:
get:
return _input_profile_id
var _input_profile_id: StringName
## Selects one supported action layout. Unknown profiles fail closed to Blizzlike335.
func _init(requested_profile_id: StringName = BLIZZLIKE_335_PROFILE_ID) -> void:
_input_profile_id = (
RENDER_SANDBOX_PROFILE_ID
if requested_profile_id == RENDER_SANDBOX_PROFILE_ID
else BLIZZLIKE_335_PROFILE_ID
)
## Builds a normalized intent from action strengths for deterministic tests and
## Samples the current Input singleton state for one physics tick.
func sample_move_intent() -> MoveIntent:
if _input_profile_id == RENDER_SANDBOX_PROFILE_ID:
return compose_move_intent(
Input.get_action_strength(PlayerInputActions.MOVE_FORWARD),
Input.get_action_strength(PlayerInputActions.MOVE_BACKWARD),
Input.get_action_strength(PlayerInputActions.STRAFE_LEFT),
Input.get_action_strength(PlayerInputActions.STRAFE_RIGHT),
Input.get_action_strength(PlayerInputActions.DEBUG_FLY_UP),
Input.get_action_strength(PlayerInputActions.DEBUG_FLY_DOWN),
Input.is_action_pressed(PlayerInputActions.DEBUG_SPRINT)
)
return compose_blizzlike_335_move_intent(
Input.get_action_strength(PlayerInputActions.MOVE_FORWARD),
Input.get_action_strength(PlayerInputActions.MOVE_BACKWARD),
Input.get_action_strength(PlayerInputActions.BLIZZLIKE_STRAFE_LEFT),
Input.get_action_strength(PlayerInputActions.BLIZZLIKE_STRAFE_RIGHT),
Input.get_action_strength(PlayerInputActions.TURN_LEFT),
Input.get_action_strength(PlayerInputActions.TURN_RIGHT),
Input.is_action_pressed(PlayerInputActions.CAMERA_ROTATE)
)
## Builds the compatibility layout: Q/E always strafe; A/D turn unless
## right-mouse camera rotation is active, when A/D also strafe. Default bindings
## come from build 12340; conditional routing is reference-implementation evidence.
static func compose_blizzlike_335_move_intent(
forward_strength: float,
backward_strength: float,
strafe_left_strength: float,
strafe_right_strength: float,
turn_left_strength: float,
turn_right_strength: float,
camera_rotate_active: bool
) -> MoveIntent:
var effective_strafe_left := strafe_left_strength
var effective_strafe_right := strafe_right_strength
var turn_axis := clampf(turn_right_strength, 0.0, 1.0) - clampf(turn_left_strength, 0.0, 1.0)
if camera_rotate_active:
effective_strafe_left += turn_left_strength
effective_strafe_right += turn_right_strength
turn_axis = 0.0
var move_intent := compose_move_intent(
forward_strength,
backward_strength,
effective_strafe_left,
effective_strafe_right,
0.0,
0.0,
false
)
return MoveIntent.new(
move_intent.forward_axis,
move_intent.strafe_axis,
0.0,
false,
turn_axis
)
## Builds a normalized sandbox intent from action strengths for deterministic tests and
## alternative input adapters. Strengths outside `[0, 1]` are clamped.
static func compose_move_intent(
forward_strength: float,
@@ -13,25 +13,34 @@ var is_flight_enabled: bool:
get:
return _is_flight_enabled
var movement_profile_id: StringName:
get:
return _movement_capabilities.profile_id
var _run_speed_units_per_second: float
var _backward_speed_units_per_second: float
var _strafe_speed_units_per_second: float
var _flight_vertical_speed_units_per_second: float
var _acceleration_units_per_second_squared: float
var _sandbox_sprint_multiplier: float
var _keyboard_turn_speed_radians_per_second: float
var _movement_capabilities: PlayerMovementCapabilities
var _godot_world_velocity_units_per_second := Vector3.ZERO
var _is_flight_enabled := false
## Creates a movement state with explicit Godot-unit speeds and acceleration.
## Creates a movement state with explicit Godot-unit speeds, acceleration and capabilities.
## The current compatibility renderer treats one Godot unit as one WoW yard.
## A null capability input fails closed to Blizzlike335 debug permissions.
func _init(
run_speed_units_per_second: float,
backward_speed_units_per_second: float,
strafe_speed_units_per_second: float,
flight_vertical_speed_units_per_second: float,
acceleration_units_per_second_squared: float,
sandbox_sprint_multiplier: float
sandbox_sprint_multiplier: float,
movement_capabilities: PlayerMovementCapabilities = null,
keyboard_turn_speed_radians_per_second: float = PI
) -> void:
_run_speed_units_per_second = run_speed_units_per_second
_backward_speed_units_per_second = backward_speed_units_per_second
@@ -39,11 +48,32 @@ func _init(
_flight_vertical_speed_units_per_second = flight_vertical_speed_units_per_second
_acceleration_units_per_second_squared = acceleration_units_per_second_squared
_sandbox_sprint_multiplier = sandbox_sprint_multiplier
_keyboard_turn_speed_radians_per_second = maxf(
keyboard_turn_speed_radians_per_second,
0.0
)
_movement_capabilities = (
movement_capabilities
if movement_capabilities != null
else PlayerMovementCapabilities.blizzlike_335()
)
## Toggles sandbox free flight and clears velocity exactly like the pre-M02 controller.
## Returns the new flight state.
## Returns the Godot yaw delta for one signed keyboard-turn request.
## Positive intent turns right, which is negative rotation around Godot +Y.
func calculate_yaw_delta_radians(move_intent: MoveIntent, delta_seconds: float) -> float:
return (
-move_intent.turn_axis
* _keyboard_turn_speed_radians_per_second
* maxf(delta_seconds, 0.0)
)
## Toggles sandbox free flight and clears velocity when the profile permits it.
## Returns the resulting state; rejected compatibility-profile requests return false.
func toggle_sandbox_flight() -> bool:
if not _movement_capabilities.allows_debug_free_flight:
return false
_is_flight_enabled = not _is_flight_enabled
_godot_world_velocity_units_per_second = Vector3.ZERO
return _is_flight_enabled
@@ -83,7 +113,11 @@ func _calculate_target_velocity(move_intent: MoveIntent, godot_world_movement_ba
if move_intent.forward_axis > 0.0
else _backward_speed_units_per_second
)
var speed_multiplier := _sandbox_sprint_multiplier if move_intent.is_sprint_requested else 1.0
var speed_multiplier := (
_sandbox_sprint_multiplier
if move_intent.is_sprint_requested and _movement_capabilities.allows_debug_sprint
else 1.0
)
var target_velocity := (
forward_direction * move_intent.forward_axis * forward_speed
+ right_direction * move_intent.strafe_axis * _strafe_speed_units_per_second
@@ -0,0 +1,53 @@
class_name PlayerMovementCapabilities
extends RefCounted
## Immutable local-player movement capability selection.
## Unknown or missing profile identifiers collapse to the safe Blizzlike335
## capability set; only RenderSandbox enables debug sprint and free flight.
const BLIZZLIKE_335_PROFILE_ID: StringName = &"Blizzlike335"
const RENDER_SANDBOX_PROFILE_ID: StringName = &"RenderSandbox"
var profile_id: StringName:
get:
return _profile_id
var allows_debug_sprint: bool:
get:
return _allows_debug_sprint
var allows_debug_free_flight: bool:
get:
return _allows_debug_free_flight
var _profile_id: StringName
var _allows_debug_sprint: bool
var _allows_debug_free_flight: bool
func _init(requested_profile_id: StringName = BLIZZLIKE_335_PROFILE_ID) -> void:
if requested_profile_id == RENDER_SANDBOX_PROFILE_ID:
_profile_id = RENDER_SANDBOX_PROFILE_ID
_allows_debug_sprint = true
_allows_debug_free_flight = true
return
_profile_id = BLIZZLIKE_335_PROFILE_ID
_allows_debug_sprint = false
_allows_debug_free_flight = false
## Returns the debug-enabled capability set used by current renderer scenes.
static func render_sandbox() -> PlayerMovementCapabilities:
return PlayerMovementCapabilities.new(RENDER_SANDBOX_PROFILE_ID)
## Returns the compatibility-safe capability set with debug movement disabled.
## This name identifies the intended compatibility profile, not proven 1:1 movement.
static func blizzlike_335() -> PlayerMovementCapabilities:
return PlayerMovementCapabilities.new(BLIZZLIKE_335_PROFILE_ID)
## Maps scene/application profile configuration to a known capability set.
## Unknown identifiers fail closed to Blizzlike335.
static func for_profile_id(requested_profile_id: StringName) -> PlayerMovementCapabilities:
return PlayerMovementCapabilities.new(requested_profile_id)
@@ -0,0 +1 @@
uid://cdnulu22drbdw
@@ -0,0 +1,84 @@
## Issues bounded, per-frame permits for renderer main-thread operation lanes.
##
## The scheduler owns counters only. Queue contents, operation ordering and the
## side effects performed after a permit remain owned by the caller. One instance
## belongs to one renderer lifecycle and must only be used on the main thread.
class_name RenderBudgetScheduler
extends RefCounted
const TILE_FINALIZE: StringName = &"tile_finalize"
const TERRAIN_UPGRADE_FINALIZE: StringName = &"terrain_upgrade_finalize"
const TERRAIN_CONTROL_SPLAT_FINALIZE: StringName = &"terrain_control_splat_finalize"
const TERRAIN_SPLAT_CACHE_FINALIZE: StringName = &"terrain_splat_cache_finalize"
const TERRAIN_SPLAT_BUILD: StringName = &"terrain_splat_build"
const WATER_FINALIZE: StringName = &"water_finalize"
const CHUNK_GEOMETRY: StringName = &"chunk_geometry"
const TILE_LOAD_START: StringName = &"tile_load_start"
const TILE_LOD_CREATE: StringName = &"tile_lod_create"
const TILE_LOD_REMOVE: StringName = &"tile_lod_remove"
const M2_ANIMATION_FINALIZE: StringName = &"m2_animation_finalize"
const M2_MESH_FINALIZE: StringName = &"m2_mesh_finalize"
const M2_BUILD: StringName = &"m2_build"
const WMO_BUILD: StringName = &"wmo_build"
const WMO_RENDER_GROUP_BUILD: StringName = &"wmo_render_group_build"
const DETAIL_ASSET_SYNC: StringName = &"detail_asset_sync"
var _remaining_permits_by_lane: Dictionary = {}
var _consumed_permits_by_lane: Dictionary = {}
var _is_cancelled := false
## Replaces all lane limits for a new frame. Negative limits are treated as zero.
## Calling this after [method cancel] leaves the scheduler cancelled.
func begin_frame(operation_limits_by_lane: Dictionary) -> void:
_remaining_permits_by_lane.clear()
_consumed_permits_by_lane.clear()
if _is_cancelled:
return
for lane_variant in operation_limits_by_lane:
var lane := StringName(lane_variant)
_remaining_permits_by_lane[lane] = maxi(0, int(operation_limits_by_lane[lane_variant]))
_consumed_permits_by_lane[lane] = 0
## Returns whether a lane can issue another permit without consuming it.
## Unknown lanes and cancelled schedulers return false.
func has_remaining_permit(lane: StringName) -> bool:
if _is_cancelled:
return false
return int(_remaining_permits_by_lane.get(lane, 0)) > 0
## Consumes one permit from [param lane] and returns true when it was granted.
## Unknown, exhausted or cancelled lanes return false without changing state.
func try_consume_permit(lane: StringName) -> bool:
if not has_remaining_permit(lane):
return false
_remaining_permits_by_lane[lane] = int(_remaining_permits_by_lane[lane]) - 1
_consumed_permits_by_lane[lane] = int(_consumed_permits_by_lane.get(lane, 0)) + 1
return true
## Returns the unconsumed permit count for one lane.
func remaining_permits(lane: StringName) -> int:
if _is_cancelled:
return 0
return int(_remaining_permits_by_lane.get(lane, 0))
## Returns a detached diagnostic snapshot of permits consumed in this frame.
func consumed_permits_snapshot() -> Dictionary:
return _consumed_permits_by_lane.duplicate()
## Permanently rejects new permits for this scheduler instance and clears limits.
## In-flight work remains the caller's responsibility and is not interrupted.
func cancel() -> void:
_is_cancelled = true
_remaining_permits_by_lane.clear()
## Returns whether [method cancel] ended this scheduler lifecycle.
func is_cancelled() -> bool:
return _is_cancelled
@@ -0,0 +1 @@
uid://byaii7lec82i6
@@ -0,0 +1,37 @@
class_name StreamingTargetPlan
extends RefCounted
## Immutable result of one runtime or editor target-planning operation.
## Tile-key sets use the streamer's existing `<tile_x>_<tile_y>` representation.
var focus_tile: AdtTileCoordinate:
get:
return _focus_tile
var _focus_tile: AdtTileCoordinate
var _wanted_tile_keys: Dictionary
var _retained_tile_keys: Dictionary
## Takes ownership of planner-created sets and freezes them. The planner creates
## these dictionaries independently from its catalog input, so no copy is needed.
func _init(
focus_tile_value: AdtTileCoordinate,
wanted_tile_keys_value: Dictionary,
retained_tile_keys_value: Dictionary
) -> void:
_focus_tile = focus_tile_value
_wanted_tile_keys = wanted_tile_keys_value
_retained_tile_keys = retained_tile_keys_value
_wanted_tile_keys.make_read_only()
_retained_tile_keys.make_read_only()
## Returns the immutable wanted tile-key set.
func wanted_tile_keys() -> Dictionary:
return _wanted_tile_keys
## Returns the immutable retained tile-key set.
func retained_tile_keys() -> Dictionary:
return _retained_tile_keys
@@ -0,0 +1 @@
uid://fq1ax8tdju2q
@@ -0,0 +1,104 @@
class_name StreamingTargetPlanner
extends RefCounted
## Scene-free deterministic calculator for runtime/editor ADT target sets.
## It creates no tasks or renderer resources and never mutates its inputs.
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
const TARGET_PLAN_SCRIPT := preload("res://src/render/streaming/streaming_target_plan.gd")
## Builds runtime wanted and retained sets, including the existing edge/corner
## prefetch centers. `available_tile_paths_by_key` is read only for membership.
func plan_runtime(
available_tile_paths_by_key: Dictionary,
focus_position: GodotWorldPosition,
policy
):
var focus_tile = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(focus_position)
var prefetch_centers := _prefetch_centers(focus_position, focus_tile, policy)
var wanted_tile_keys := _available_keys_within_radius(
available_tile_paths_by_key,
prefetch_centers,
policy.load_tile_radius()
)
var retained_tile_keys := _available_keys_within_radius(
available_tile_paths_by_key,
prefetch_centers,
policy.retained_tile_radius()
)
return TARGET_PLAN_SCRIPT.new(focus_tile, wanted_tile_keys, retained_tile_keys)
## Builds the editor square preview set. Editor wanted and retained sets are
## identical and a negative radius is clamped to the focus tile.
func plan_editor(
available_tile_paths_by_key: Dictionary,
focus_position: GodotWorldPosition,
editor_preview_tile_radius: int
):
var focus_tile = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(focus_position)
var centers: Array[AdtTileCoordinate] = [focus_tile]
var wanted_tile_keys := _available_keys_within_radius(
available_tile_paths_by_key,
centers,
maxi(0, editor_preview_tile_radius)
)
return TARGET_PLAN_SCRIPT.new(focus_tile, wanted_tile_keys, wanted_tile_keys)
func _prefetch_centers(
focus_position: GodotWorldPosition,
focus_tile: AdtTileCoordinate,
policy
) -> Array[AdtTileCoordinate]:
var centers: Array[AdtTileCoordinate] = [focus_tile]
var threshold: float = policy.clamped_boundary_prefetch_threshold()
if threshold <= 0.0:
return centers
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(focus_position)
var local_x_fraction: float = tile_local_position.east_yards / COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
var local_z_fraction: float = tile_local_position.south_yards / COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
var tile_x_offsets: Array[int] = [0]
var tile_y_offsets: Array[int] = [0]
if local_x_fraction <= threshold:
tile_x_offsets.append(-1)
elif local_x_fraction >= 1.0 - threshold:
tile_x_offsets.append(1)
if local_z_fraction <= threshold:
tile_y_offsets.append(-1)
elif local_z_fraction >= 1.0 - threshold:
tile_y_offsets.append(1)
for tile_x_offset in tile_x_offsets:
for tile_y_offset in tile_y_offsets:
if tile_x_offset == 0 and tile_y_offset == 0:
continue
centers.append(ADT_TILE_COORDINATE_SCRIPT.new(
focus_tile.tile_x + tile_x_offset,
focus_tile.tile_y + tile_y_offset
))
return centers
func _available_keys_within_radius(
available_tile_paths_by_key: Dictionary,
centers: Array[AdtTileCoordinate],
tile_radius: int
) -> Dictionary:
var available_tile_keys: Dictionary = {}
for center in centers:
for tile_y in range(center.tile_y - tile_radius, center.tile_y + tile_radius + 1):
for tile_x in range(center.tile_x - tile_radius, center.tile_x + tile_radius + 1):
var tile_key := _tile_key(tile_x, tile_y)
if available_tile_paths_by_key.has(tile_key):
available_tile_keys[tile_key] = true
return available_tile_keys
func _tile_key(tile_x: int, tile_y: int) -> String:
return "%d_%d" % [tile_x, tile_y]
@@ -0,0 +1 @@
uid://crqjbgoe3f6ea
@@ -0,0 +1,108 @@
class_name StreamingTargetPolicy
extends RefCounted
## Immutable runtime tile-radius and boundary-prefetch policy consumed by
## [StreamingTargetPlanner]. Values preserve scene configuration exactly;
## effective radii and thresholds apply the existing renderer clamps.
const CHUNKS_PER_ADT_TILE := 16
var lod2_radius_chunks: int:
get:
return _lod2_radius_chunks
var lod2_tile_radius: int:
get:
return _lod2_tile_radius
var warm_tile_margin: int:
get:
return _warm_tile_margin
var prewarm_tile_margin: int:
get:
return _prewarm_tile_margin
var retain_tile_margin: int:
get:
return _retain_tile_margin
var boundary_prefetch_threshold: float:
get:
return _boundary_prefetch_threshold
var _lod2_radius_chunks: int
var _lod2_tile_radius: int
var _warm_tile_margin: int
var _prewarm_tile_margin: int
var _retain_tile_margin: int
var _boundary_prefetch_threshold: float
## Captures one renderer configuration snapshot for a deterministic plan.
func _init(
lod2_radius_chunks_value: int,
lod2_tile_radius_value: int,
warm_tile_margin_value: int,
prewarm_tile_margin_value: int,
retain_tile_margin_value: int,
boundary_prefetch_threshold_value: float
) -> void:
_lod2_radius_chunks = lod2_radius_chunks_value
_lod2_tile_radius = lod2_tile_radius_value
_warm_tile_margin = warm_tile_margin_value
_prewarm_tile_margin = prewarm_tile_margin_value
_retain_tile_margin = retain_tile_margin_value
_boundary_prefetch_threshold = boundary_prefetch_threshold_value
## Returns the current visible ADT radius derived from chunk and tile settings.
func visible_tile_radius() -> int:
return calculate_visible_tile_radius(_lod2_radius_chunks, _lod2_tile_radius, _warm_tile_margin)
## Returns the visible radius plus the non-negative prewarm margin.
func load_tile_radius() -> int:
return calculate_load_tile_radius(
_lod2_radius_chunks,
_lod2_tile_radius,
_warm_tile_margin,
_prewarm_tile_margin
)
## Returns the load radius plus the non-negative hysteresis margin.
func retained_tile_radius() -> int:
return load_tile_radius() + maxi(0, _retain_tile_margin)
## Returns the edge fraction accepted by the existing prefetch policy.
func clamped_boundary_prefetch_threshold() -> float:
return clampf(_boundary_prefetch_threshold, 0.0, 0.49)
## Calculates the visible radius without allocating a policy. The streamer uses
## this hot-path form while finalizing tiles between planning refreshes.
static func calculate_visible_tile_radius(
lod2_radius_chunks_value: int,
lod2_tile_radius_value: int,
warm_tile_margin_value: int
) -> int:
var chunk_tile_radius := int(ceil(
float(lod2_radius_chunks_value) / float(CHUNKS_PER_ADT_TILE)
)) + warm_tile_margin_value
return maxi(chunk_tile_radius, lod2_tile_radius_value)
## Calculates the load radius without allocating a policy.
static func calculate_load_tile_radius(
lod2_radius_chunks_value: int,
lod2_tile_radius_value: int,
warm_tile_margin_value: int,
prewarm_tile_margin_value: int
) -> int:
return calculate_visible_tile_radius(
lod2_radius_chunks_value,
lod2_tile_radius_value,
warm_tile_margin_value
) + maxi(0, prewarm_tile_margin_value)
@@ -0,0 +1 @@
uid://cfrad5wr668w5
+110
View File
@@ -0,0 +1,110 @@
@tool
class_name WorldRenderFacade
extends Node
## Stable main-thread boundary between runtime/tool callers and the monolithic
## streaming renderer. The facade owns no queues, caches, nodes or RIDs.
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
## Internal renderer implementation resolved relative to this node.
## The referenced node remains owned by its scene parent.
@export var streaming_world_loader_path: NodePath = NodePath("..")
## Optional explicit Node3D source sampled in Godot world coordinates.
## Runtime scenes use the player; capture and probe tools replace it with their camera.
@export var streaming_focus_source_path: NodePath
var _streaming_world_loader: Node
var _missing_loader_reported := false
var _missing_focus_source_reported := false
func _ready() -> void:
_resolve_streaming_world_loader()
set_process(streaming_focus_source_path != NodePath())
if streaming_focus_source_path != NodePath():
# Child nodes become ready before their parent StreamingWorldLoader. Only
# publish the initial focus here; the loader applies it after initialization.
_capture_streaming_focus_from_source()
func _process(_delta_seconds: float) -> void:
_capture_streaming_focus_from_source()
## Replaces the renderer focus. The immutable value remains caller-owned; the
## internal streamer retains the reference for its next throttled refresh.
func set_streaming_focus(streaming_focus: StreamingFocus) -> void:
var loader := _resolve_streaming_world_loader()
if loader == null:
return
loader.call("set_streaming_focus", streaming_focus)
## Samples the configured source and requests target refresh from the internal
## streamer. Returns false when either dependency or a valid focus is unavailable.
func refresh_streaming_focus(force: bool = false) -> bool:
_capture_streaming_focus_from_source()
var loader := _resolve_streaming_world_loader()
if loader == null:
return false
return bool(loader.call("refresh_streaming_focus", force))
## Returns a detached read-only metrics snapshot for diagnostics and baselines.
## Mutating the returned Dictionary cannot mutate renderer-owned state.
func renderer_metrics_snapshot() -> Dictionary:
var loader := _resolve_streaming_world_loader()
if loader == null:
return {}
var metrics_variant = loader.call("render_baseline_snapshot")
if not (metrics_variant is Dictionary):
push_error("WorldRenderFacade received a non-Dictionary renderer metrics snapshot.")
return {}
return (metrics_variant as Dictionary).duplicate(true)
func _capture_streaming_focus_from_source() -> void:
if streaming_focus_source_path == NodePath():
return
var focus_source := get_node_or_null(streaming_focus_source_path) as Node3D
if focus_source == null:
if not _missing_focus_source_reported:
push_warning("WorldRenderFacade focus source is missing or is not Node3D: %s" % streaming_focus_source_path)
_missing_focus_source_reported = true
return
_missing_focus_source_reported = false
var godot_position: Vector3 = focus_source.global_position
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(
godot_position.x,
godot_position.y,
godot_position.z
)
set_streaming_focus(STREAMING_FOCUS_SCRIPT.new(typed_world_position))
func _resolve_streaming_world_loader() -> Node:
if is_instance_valid(_streaming_world_loader):
return _streaming_world_loader
var candidate := get_node_or_null(streaming_world_loader_path)
if candidate == null:
if not _missing_loader_reported:
push_error("WorldRenderFacade cannot resolve StreamingWorldLoader at %s" % streaming_world_loader_path)
_missing_loader_reported = true
return null
for required_method in [
&"set_streaming_focus",
&"refresh_streaming_focus",
&"render_baseline_snapshot",
]:
if not candidate.has_method(required_method):
if not _missing_loader_reported:
push_error("WorldRenderFacade renderer implementation lacks %s" % required_method)
_missing_loader_reported = true
return null
_streaming_world_loader = candidate
_missing_loader_reported = false
return _streaming_world_loader
+1
View File
@@ -0,0 +1 @@
uid://sxn3ai2ic18r
@@ -94,6 +94,12 @@ func initialize_for_character(character_body: Node3D) -> void:
refresh_camera_transform()
## Synchronizes orbit yaw after keyboard turning changes the character transform.
func synchronize_yaw_from_character() -> void:
if _character_body != null:
_yaw_radians = _character_body.rotation.y
## Replaces camera-distance collision behavior and immediately refreshes the camera.
## Passing null is rejected and preserves the current policy.
func set_collision_policy(collision_policy: CameraCollisionPolicy) -> void:
@@ -7,6 +7,7 @@ const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/ad
const PLAYER_INPUT_SOURCE_SCRIPT := preload("res://src/gameplay/input/player_input_source.gd")
const PLAYER_INPUT_ACTIONS_SCRIPT := preload("res://src/gameplay/input/player_input_actions.gd")
const LOCAL_PLAYER_MOVEMENT_CONTROLLER_SCRIPT := preload("res://src/gameplay/movement/local_player_movement_controller.gd")
const PLAYER_MOVEMENT_CAPABILITIES_SCRIPT := preload("res://src/gameplay/movement/player_movement_capabilities.gd")
const ADT_TERRAIN_QUERY_SCRIPT := preload("res://src/gameplay/terrain/adt_terrain_query.gd")
@export var extracted_dir: String = "res://data/extracted"
@@ -21,6 +22,11 @@ const ADT_TERRAIN_QUERY_SCRIPT := preload("res://src/gameplay/terrain/adt_terrai
@export var sprint_multiplier: float = 6.0
@export var flight_vertical_speed: float = 7.0
@export var acceleration: float = 28.0
## TrinityCore 3.3.5 player MOVE_TURN_RATE default, in radians per second.
@export var keyboard_turn_speed_radians_per_second: float = 3.141594
## Local composition profile. RenderSandbox preserves debug sprint/free flight;
## Blizzlike335 rejects them but does not yet claim complete movement parity.
@export_enum("RenderSandbox", "Blizzlike335") var movement_profile_id: String = "RenderSandbox"
@export var ground_offset: float = 0.05
@export var ground_snap_speed: float = 24.0
@@ -46,7 +52,7 @@ func set_terrain_query(terrain_query: TerrainQuery) -> void:
func _ready() -> void:
_player_input_source = PLAYER_INPUT_SOURCE_SCRIPT.new()
_player_input_source = PLAYER_INPUT_SOURCE_SCRIPT.new(StringName(movement_profile_id))
if _terrain_query == null:
_terrain_query = ADT_TERRAIN_QUERY_SCRIPT.new(extracted_dir, map_name)
_local_movement_controller = LOCAL_PLAYER_MOVEMENT_CONTROLLER_SCRIPT.new(
@@ -55,7 +61,9 @@ func _ready() -> void:
strafe_speed,
flight_vertical_speed,
acceleration,
sprint_multiplier
sprint_multiplier,
PLAYER_MOVEMENT_CAPABILITIES_SCRIPT.for_profile_id(StringName(movement_profile_id)),
keyboard_turn_speed_radians_per_second
)
_camera_rig = get_node_or_null(camera_pivot_path) as ThirdPersonCameraRig
_appearance_presenter = get_node_or_null(visual_path) as CharacterAppearancePresenter
@@ -91,6 +99,14 @@ func _unhandled_input(event: InputEvent) -> void:
func _physics_process(delta: float) -> void:
var move_intent := _player_input_source.sample_move_intent()
var yaw_delta_radians := _local_movement_controller.calculate_yaw_delta_radians(
move_intent,
delta
)
if not is_zero_approx(yaw_delta_radians):
rotation.y += yaw_delta_radians
if _camera_rig != null:
_camera_rig.synchronize_yaw_from_character()
var godot_world_movement_basis := global_basis
if _local_movement_controller.is_flight_enabled and _camera_rig != null:
godot_world_movement_basis = _camera_rig.godot_world_flight_movement_basis()
@@ -3,9 +3,10 @@
[ext_resource type="Script" uid="uid://yi6lawwjgocg" path="res://src/scenes/streaming/streaming_world_loader.gd" id="1_stream"]
[ext_resource type="Script" uid="uid://ltn2ko5kxvo4" path="res://src/scenes/player/third_person_wow_controller.gd" id="2_player"]
[ext_resource type="Script" uid="uid://didjth34ut0v1" path="res://src/scenes/sky/wow_sky_controller.gd" id="3_sky"]
[ext_resource type="Script" path="res://src/scenes/player/third_person_camera_rig.gd" id="4_camera_rig"]
[ext_resource type="Script" path="res://src/scenes/character/character_appearance_presenter.gd" id="5_appearance"]
[ext_resource type="Script" path="res://src/scenes/character/character_animation_presenter.gd" id="6_animation"]
[ext_resource type="Script" uid="uid://cq60al4uw0mgo" path="res://src/scenes/player/third_person_camera_rig.gd" id="4_camera_rig"]
[ext_resource type="Script" uid="uid://bhqbo1ftrt3a3" path="res://src/scenes/character/character_appearance_presenter.gd" id="5_appearance"]
[ext_resource type="Script" uid="uid://c2jnn0e3mmymc" path="res://src/scenes/character/character_animation_presenter.gd" id="6_animation"]
[ext_resource type="Script" path="res://src/render/world_render_facade.gd" id="7_render_facade"]
[sub_resource type="CapsuleShape3D" id="PlayerCapsuleShape_1"]
radius = 0.45
@@ -56,7 +57,6 @@ fog_depth_end = 5200.0
[node name="StreamingWorld" type="Node3D" unique_id=1063159974]
script = ExtResource("1_stream")
streaming_focus_source_path = NodePath("ThirdPersonPlayer")
camera_path = NodePath("ThirdPersonPlayer/CameraPivot/Camera3D")
quality_preset = "High"
update_interval = 0.1
@@ -94,11 +94,17 @@ wmo_visibility_range = 3600.0
debug_streaming = true
hitch_profiler_enabled = true
[node name="WorldRenderFacade" type="Node" parent="."]
script = ExtResource("7_render_facade")
streaming_world_loader_path = NodePath("..")
streaming_focus_source_path = NodePath("../ThirdPersonPlayer")
[node name="ThirdPersonPlayer" type="CharacterBody3D" parent="." unique_id=502573687]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 16800, 80, 26400)
script = ExtResource("2_player")
spawn_tile_x = 31
spawn_tile_y = 49
movement_profile_id = "Blizzlike335"
[node name="CollisionShape3D" type="CollisionShape3D" parent="ThirdPersonPlayer" unique_id=1297880621]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.05, 0)
@@ -109,7 +115,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.05, 0)
mesh = SubResource("PlayerCapsuleMesh_1")
script = ExtResource("5_appearance")
[node name="AnimationPresenter" type="Node" parent="ThirdPersonPlayer/Visual"]
[node name="AnimationPresenter" type="Node" parent="ThirdPersonPlayer/Visual" unique_id=2131488264]
script = ExtResource("6_animation")
[node name="CameraPivot" type="Node3D" parent="ThirdPersonPlayer" unique_id=499263249]
+6 -1
View File
@@ -6,6 +6,7 @@
[ext_resource type="Script" path="res://src/scenes/player/third_person_camera_rig.gd" id="4_camera_rig"]
[ext_resource type="Script" path="res://src/scenes/character/character_appearance_presenter.gd" id="5_appearance"]
[ext_resource type="Script" path="res://src/scenes/character/character_animation_presenter.gd" id="6_animation"]
[ext_resource type="Script" path="res://src/render/world_render_facade.gd" id="7_render_facade"]
[sub_resource type="CapsuleShape3D" id="PlayerCapsuleShape_1"]
radius = 0.45
@@ -57,7 +58,6 @@ fog_depth_end = 5200.0
[node name="StreamingWorld" type="Node3D" unique_id=1063159974]
script = ExtResource("1_sisqv")
map_name = "Kalimdor"
streaming_focus_source_path = NodePath("ThirdPersonPlayer")
camera_path = NodePath("ThirdPersonPlayer/CameraPivot/Camera3D")
quality_preset = "High"
update_interval = 0.1
@@ -95,6 +95,11 @@ wmo_visibility_range = 3600.0
debug_streaming = true
hitch_profiler_enabled = true
[node name="WorldRenderFacade" type="Node" parent="."]
script = ExtResource("7_render_facade")
streaming_world_loader_path = NodePath("..")
streaming_focus_source_path = NodePath("../ThirdPersonPlayer")
[node name="ThirdPersonPlayer" type="CharacterBody3D" parent="." unique_id=502573687]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 16800, 80, 26400)
script = ExtResource("2_puy8r")
+123 -143
View File
@@ -17,6 +17,9 @@ const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
const STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
const REQUIRED_BAKED_TILE_FORMAT_VERSION := 5
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
@@ -174,6 +177,7 @@ const QUALITY_HIGH := "High"
@export var hitch_profiler_threshold_ms: float = 20.0
var _builder
var _streaming_target_planner := STREAMING_TARGET_PLANNER_SCRIPT.new()
var _map_dir := ""
var _abs_map_dir := ""
var _wdt_info: Dictionary = {}
@@ -230,6 +234,7 @@ var _dbg_last_report := 0.0
var _runtime_stats_accum := 0.0
var _terrain_quality_log_accum := 0.0
var _shutting_down := false
var _render_budget_scheduler := RENDER_BUDGET_SCHEDULER_SCRIPT.new()
var _last_refresh_focus_pos := Vector3.ZERO
var _has_refresh_focus := false
var _tile_mesh_cache: Dictionary = {}
@@ -313,6 +318,7 @@ func _ready() -> void:
func _process(delta: float) -> void:
if _available_tiles.is_empty():
return
_render_budget_scheduler.begin_frame(_render_operation_limits_for_frame())
var profile_enabled := hitch_profiler_enabled and not Engine.is_editor_hint()
var profile_start := Time.get_ticks_usec() if profile_enabled else 0
@@ -384,11 +390,33 @@ func _process(delta: float) -> void:
func _exit_tree() -> void:
_shutting_down = true
_render_budget_scheduler.cancel()
_wait_for_tile_tasks()
_clear_streamed_world()
_release_runtime_caches_for_shutdown()
func _render_operation_limits_for_frame() -> Dictionary:
return {
RENDER_BUDGET_SCHEDULER_SCRIPT.TILE_FINALIZE: maxi(1, tile_finalize_ops_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_UPGRADE_FINALIZE: maxi(0, terrain_upgrade_finalize_ops_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_CONTROL_SPLAT_FINALIZE: maxi(0, terrain_control_splat_cache_finalize_ops_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_SPLAT_CACHE_FINALIZE: maxi(0, terrain_splat_cache_finalize_ops_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_SPLAT_BUILD: maxi(0, terrain_splat_builds_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.WATER_FINALIZE: maxi(0, water_finalize_ops_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.CHUNK_GEOMETRY: maxi(0, chunk_ops_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.TILE_LOAD_START: maxi(0, tiles_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.TILE_LOD_CREATE: maxi(0, tile_lod_ops_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.TILE_LOD_REMOVE: maxi(0, tile_lod_remove_ops_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE: maxi(0, m2_animation_finalize_ops_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_MESH_FINALIZE: maxi(0, m2_mesh_finalize_ops_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_BUILD: maxi(0, m2_build_groups_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_BUILD: maxi(0, wmo_build_instances_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_RENDER_GROUP_BUILD: maxi(0, wmo_render_group_ops_per_tick),
RENDER_BUDGET_SCHEDULER_SCRIPT.DETAIL_ASSET_SYNC: maxi(0, detail_asset_ops_per_tick),
}
## Frees detached prototype nodes and releases render resources owned by this
## loader after all asynchronous work has completed. Map refreshes retain these
## caches; only scene shutdown destroys them.
@@ -720,57 +748,47 @@ func _refresh_editor_streaming_targets_at(focus_pos: Vector3, force: bool) -> vo
if not force and not _should_refresh_focus(focus_pos):
return
var focus_tile := _world_to_tile(focus_pos)
var total_tile_radius: int = maxi(0, editor_preview_tile_radius)
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(focus_pos.x, focus_pos.y, focus_pos.z)
var target_plan = _streaming_target_planner.plan_editor(
_available_tiles,
typed_focus_position,
editor_preview_tile_radius
)
var wanted_tiles: Dictionary = target_plan.wanted_tile_keys()
var wanted_tiles: Dictionary = {}
for ty in range(focus_tile.y - total_tile_radius, focus_tile.y + total_tile_radius + 1):
for tx in range(focus_tile.x - total_tile_radius, focus_tile.x + total_tile_radius + 1):
var key := _tile_key(tx, ty)
if _available_tiles.has(key):
wanted_tiles[key] = true
_apply_streaming_target(wanted_tiles, wanted_tiles, focus_pos)
_apply_streaming_target(wanted_tiles, target_plan.retained_tile_keys(), focus_pos)
_note_refresh_focus(focus_pos)
if force or debug_streaming:
print("StreamingWorld(editor): focus_tile=%s wanted=%d queued=%d" % [
focus_tile, wanted_tiles.size(), _tile_load_queue.size()])
Vector2i(target_plan.focus_tile.tile_x, target_plan.focus_tile.tile_y),
wanted_tiles.size(),
_tile_load_queue.size(),
])
func _refresh_streaming_targets_at(focus_pos: Vector3, force: bool) -> void:
if not force and not _should_refresh_focus(focus_pos):
return
var focus_tile := _world_to_tile(focus_pos)
# Chunk-LOD radius → tile radius (ceiling), plus margin for pre-warming.
var focus_tiles := _predictive_focus_tiles(focus_pos)
var total_tile_radius: int = _runtime_load_tile_radius()
var retain_tile_radius: int = _runtime_retain_tile_radius()
var retained_tiles: Dictionary = {}
for center in focus_tiles:
for ty in range(center.y - retain_tile_radius, center.y + retain_tile_radius + 1):
for tx in range(center.x - retain_tile_radius, center.x + retain_tile_radius + 1):
var retained_key := _tile_key(tx, ty)
if _available_tiles.has(retained_key):
retained_tiles[retained_key] = true
var wanted_tiles: Dictionary = {}
for center in focus_tiles:
for ty in range(center.y - total_tile_radius, center.y + total_tile_radius + 1):
for tx in range(center.x - total_tile_radius, center.x + total_tile_radius + 1):
var key := _tile_key(tx, ty)
if _available_tiles.has(key):
wanted_tiles[key] = true
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(focus_pos.x, focus_pos.y, focus_pos.z)
var target_plan = _streaming_target_planner.plan_runtime(
_available_tiles,
typed_focus_position,
_streaming_target_policy()
)
var wanted_tiles: Dictionary = target_plan.wanted_tile_keys()
var retained_tiles: Dictionary = target_plan.retained_tile_keys()
_apply_streaming_target(wanted_tiles, retained_tiles, focus_pos)
_note_refresh_focus(focus_pos)
if force or debug_streaming:
print("StreamingWorld: focus_tile=%s wanted=%d queued=%d" % [
focus_tile, wanted_tiles.size(), _tile_load_queue.size()])
Vector2i(target_plan.focus_tile.tile_x, target_plan.focus_tile.tile_y),
wanted_tiles.size(),
_tile_load_queue.size(),
])
@@ -916,33 +934,31 @@ func _rebuild_chunk_queues() -> void:
func _process_queues() -> void:
var ops_left: int = chunk_ops_per_tick
var tile_lod_create_left: int = tile_lod_ops_per_tick
var tile_lod_remove_left: int = tile_lod_remove_ops_per_tick
# Free stale terrain first so GPU descriptors/memory are reclaimed before new creates.
while ops_left > 0 and not _chunk_remove_queue.is_empty():
while not _chunk_remove_queue.is_empty() and _render_budget_scheduler.try_consume_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.CHUNK_GEOMETRY):
_remove_chunk(_chunk_remove_queue.pop_back())
ops_left -= 1
while tile_lod_remove_left > 0 and not _tile_lod_remove_queue.is_empty():
while not _tile_lod_remove_queue.is_empty() and _render_budget_scheduler.try_consume_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.TILE_LOD_REMOVE):
_remove_tile_lod(_tile_lod_remove_queue.pop_back())
tile_lod_remove_left -= 1
# Parse N ADT files this tick — the main throttle knob
var loads_left: int = tiles_per_tick
while loads_left > 0 and not _tile_load_queue.is_empty() and _tile_loading_tasks.size() < max_concurrent_tile_tasks:
while (
not _tile_load_queue.is_empty()
and _tile_loading_tasks.size() < max_concurrent_tile_tasks
and _render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.TILE_LOAD_START)
):
_start_tile_load_async(_tile_load_queue.pop_front())
loads_left -= 1
while tile_lod_create_left > 0 and not _tile_lod_create_queue.is_empty():
while not _tile_lod_create_queue.is_empty() and _render_budget_scheduler.try_consume_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.TILE_LOD_CREATE):
_create_tile_lod(_tile_lod_create_queue.pop_front())
tile_lod_create_left -= 1
# Create chunk meshes — sorted nearest-first so visible detail appears first
while ops_left > 0 and not _chunk_create_queue.is_empty():
while not _chunk_create_queue.is_empty() and _render_budget_scheduler.try_consume_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.CHUNK_GEOMETRY):
_create_chunk(_chunk_create_queue.pop_front())
ops_left -= 1
# Debug summary every 2 seconds
_dbg_last_report += get_process_delta_time()
@@ -1293,11 +1309,10 @@ func _load_tile_task(request: Dictionary) -> void:
func _drain_tile_load_results() -> void:
var results: Array = []
var finalize_budget: int = maxi(1, tile_finalize_ops_per_tick)
_tile_result_mutex.lock()
while finalize_budget > 0 and not _tile_result_queue.is_empty():
while not _tile_result_queue.is_empty() and _render_budget_scheduler.try_consume_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.TILE_FINALIZE):
results.append(_tile_result_queue.pop_front())
finalize_budget -= 1
_tile_result_mutex.unlock()
for result in results:
@@ -1321,10 +1336,10 @@ func _drain_tile_load_results() -> void:
_finalize_loaded_tile(request, result["data"])
var baked_ready: Array = []
if finalize_budget <= 0:
if not _render_budget_scheduler.has_remaining_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.TILE_FINALIZE):
return
for key in _tile_loading_tasks.keys():
if baked_ready.size() >= finalize_budget:
if not _render_budget_scheduler.has_remaining_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.TILE_FINALIZE):
break
var pending: Dictionary = _tile_loading_tasks[key]
if pending.get("mode", "raw") != "baked" and pending.get("mode", "raw") != "stream":
@@ -1332,7 +1347,8 @@ func _drain_tile_load_results() -> void:
var status := ResourceLoader.load_threaded_get_status(pending["path"])
if status == ResourceLoader.THREAD_LOAD_LOADED or status == ResourceLoader.THREAD_LOAD_FAILED:
baked_ready.append(key)
if _render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.TILE_FINALIZE):
baked_ready.append(key)
for key in baked_ready:
if not _tile_loading_tasks.has(key):
@@ -1750,18 +1766,18 @@ func _request_terrain_splat_cache(state: Dictionary) -> bool:
func _drain_terrain_upgrade_results() -> void:
var ready: Array[String] = []
var budget := maxi(0, terrain_upgrade_finalize_ops_per_tick)
if budget <= 0:
if not _render_budget_scheduler.has_remaining_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_UPGRADE_FINALIZE):
return
for key_variant in _terrain_upgrade_tasks.keys():
if ready.size() >= budget:
if not _render_budget_scheduler.has_remaining_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_UPGRADE_FINALIZE):
break
var key := String(key_variant)
var pending: Dictionary = _terrain_upgrade_tasks[key]
var status := ResourceLoader.load_threaded_get_status(String(pending.get("path", "")))
if status == ResourceLoader.THREAD_LOAD_LOADED or status == ResourceLoader.THREAD_LOAD_FAILED:
ready.append(key)
if _render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_UPGRADE_FINALIZE):
ready.append(key)
var changed := false
for key in ready:
@@ -1809,18 +1825,18 @@ func _drain_terrain_upgrade_results() -> void:
func _drain_terrain_control_splat_cache_results() -> void:
var ready: Array[String] = []
var budget := maxi(0, terrain_control_splat_cache_finalize_ops_per_tick)
if budget <= 0:
if not _render_budget_scheduler.has_remaining_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_CONTROL_SPLAT_FINALIZE):
return
for key_variant in _terrain_control_splat_cache_tasks.keys():
if ready.size() >= budget:
if not _render_budget_scheduler.has_remaining_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_CONTROL_SPLAT_FINALIZE):
break
var key := String(key_variant)
var pending: Dictionary = _terrain_control_splat_cache_tasks[key]
var status := ResourceLoader.load_threaded_get_status(String(pending.get("path", "")))
if status == ResourceLoader.THREAD_LOAD_LOADED or status == ResourceLoader.THREAD_LOAD_FAILED:
ready.append(key)
if _render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_CONTROL_SPLAT_FINALIZE):
ready.append(key)
var changed := false
for key in ready:
@@ -1875,18 +1891,18 @@ func _drain_terrain_control_splat_cache_results() -> void:
func _drain_terrain_splat_cache_results() -> void:
var ready: Array[String] = []
var budget := maxi(0, terrain_splat_cache_finalize_ops_per_tick)
if budget <= 0:
if not _render_budget_scheduler.has_remaining_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_SPLAT_CACHE_FINALIZE):
return
for key_variant in _terrain_splat_cache_tasks.keys():
if ready.size() >= budget:
if not _render_budget_scheduler.has_remaining_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_SPLAT_CACHE_FINALIZE):
break
var key := String(key_variant)
var pending: Dictionary = _terrain_splat_cache_tasks[key]
var status := ResourceLoader.load_threaded_get_status(String(pending.get("path", "")))
if status == ResourceLoader.THREAD_LOAD_LOADED or status == ResourceLoader.THREAD_LOAD_FAILED:
ready.append(key)
if _render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_SPLAT_CACHE_FINALIZE):
ready.append(key)
var changed := false
for key in ready:
@@ -1963,14 +1979,13 @@ func _load_terrain_splat_task(key: String, adt_path: String) -> void:
func _drain_terrain_splat_results() -> void:
var results: Array = []
var budget := maxi(0, terrain_splat_builds_per_tick)
if budget <= 0:
if not _render_budget_scheduler.has_remaining_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_SPLAT_BUILD):
return
_terrain_splat_result_mutex.lock()
while budget > 0 and not _terrain_splat_result_queue.is_empty():
while not _terrain_splat_result_queue.is_empty() and _render_budget_scheduler.try_consume_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.TERRAIN_SPLAT_BUILD):
results.append(_terrain_splat_result_queue.pop_front())
budget -= 1
_terrain_splat_result_mutex.unlock()
var changed := false
@@ -2074,14 +2089,13 @@ func _load_tile_water_task(key: String, adt_path: String) -> void:
func _drain_water_load_results() -> void:
var results: Array = []
var budget := maxi(0, water_finalize_ops_per_tick)
if budget <= 0:
if not _render_budget_scheduler.has_remaining_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.WATER_FINALIZE):
return
_water_result_mutex.lock()
while budget > 0 and not _water_result_queue.is_empty():
while not _water_result_queue.is_empty() and _render_budget_scheduler.try_consume_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.WATER_FINALIZE):
results.append(_water_result_queue.pop_front())
budget -= 1
_water_result_mutex.unlock()
for result in results:
@@ -2891,50 +2905,24 @@ func _compute_desired_lods(state: Dictionary, cam_pos: Vector3) -> Dictionary:
return desired
func _runtime_visible_tile_radius() -> int:
var chunk_tile_radius: int = int(ceil(float(lod2_radius_chunks) / 16.0)) + warm_tile_margin
return maxi(chunk_tile_radius, lod2_tile_radius)
func _runtime_load_tile_radius() -> int:
return _runtime_visible_tile_radius() + maxi(0, prewarm_tile_margin)
return STREAMING_TARGET_POLICY_SCRIPT.calculate_load_tile_radius(
lod2_radius_chunks,
lod2_tile_radius,
warm_tile_margin,
prewarm_tile_margin
)
func _runtime_retain_tile_radius() -> int:
return _runtime_load_tile_radius() + maxi(0, retain_tile_margin)
func _predictive_focus_tiles(focus_pos: Vector3) -> Array[Vector2i]:
var base := _world_to_tile(focus_pos)
var result: Array[Vector2i] = [base]
var threshold: float = clampf(boundary_prefetch_threshold, 0.0, 0.49)
if threshold <= 0.0:
return result
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(focus_pos.x, focus_pos.y, focus_pos.z)
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(typed_focus_position)
var local_x: float = tile_local_position.east_yards / TILE_SIZE
var local_z: float = tile_local_position.south_yards / TILE_SIZE
var x_offsets: Array[int] = [0]
var z_offsets: Array[int] = [0]
if local_x <= threshold:
x_offsets.append(-1)
elif local_x >= 1.0 - threshold:
x_offsets.append(1)
if local_z <= threshold:
z_offsets.append(-1)
elif local_z >= 1.0 - threshold:
z_offsets.append(1)
for ox in x_offsets:
for oz in z_offsets:
if ox == 0 and oz == 0:
continue
result.append(Vector2i(base.x + ox, base.y + oz))
return result
func _streaming_target_policy():
return STREAMING_TARGET_POLICY_SCRIPT.new(
lod2_radius_chunks,
lod2_tile_radius,
warm_tile_margin,
prewarm_tile_margin,
retain_tile_margin,
boundary_prefetch_threshold
)
func _compute_desired_tile_lod(state: Dictionary, cam_pos: Vector3) -> int:
@@ -3285,12 +3273,11 @@ func _enqueue_detail_asset_sync(key: String) -> void:
func _process_detail_asset_queue() -> void:
var ops_left: int = maxi(0, detail_asset_ops_per_tick)
while ops_left > 0 and not _detail_asset_queue.is_empty():
while not _detail_asset_queue.is_empty() and _render_budget_scheduler.try_consume_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.DETAIL_ASSET_SYNC):
var key: String = String(_detail_asset_queue.pop_front())
_detail_asset_queued.erase(key)
_process_detail_asset_key(key)
ops_left -= 1
func _process_detail_asset_key(key: String) -> void:
@@ -3374,8 +3361,8 @@ func _register_tile_wmos(state: Dictionary, wmo_names: PackedStringArray, wmo_pl
func _process_wmo_build_jobs() -> void:
_drain_wmo_render_loads()
_drain_wmo_scene_loads()
var ops_left: int = maxi(0, wmo_build_instances_per_tick)
while ops_left > 0 and not _wmo_build_queue.is_empty():
while _render_budget_scheduler.has_remaining_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_BUILD) and not _wmo_build_queue.is_empty():
var tile_key: String = String(_wmo_build_queue.front())
if not _wmo_build_jobs.has(tile_key):
_wmo_build_queue.pop_front()
@@ -3472,7 +3459,7 @@ func _process_wmo_build_jobs() -> void:
_tile_states[tile_key] = state
_wmo_build_queue.pop_front()
_wmo_build_queue.append(tile_key)
ops_left -= 1
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_BUILD)
continue
job["index"] = index + 1
@@ -3480,7 +3467,7 @@ func _process_wmo_build_jobs() -> void:
state["wmo_refs"] = refs
state["wmo_building"] = true
_tile_states[tile_key] = state
ops_left -= 1
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_BUILD)
if int(job["index"]) >= wmo_placements.size():
state["wmo_refs"] = refs
@@ -3622,8 +3609,8 @@ func _start_wmo_render_build(unique_key: String, root: Node3D, render_resource:
func _process_wmo_render_build_jobs() -> void:
var ops_left: int = maxi(0, wmo_render_group_ops_per_tick)
while ops_left > 0 and not _wmo_render_build_queue.is_empty():
while _render_budget_scheduler.has_remaining_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_RENDER_GROUP_BUILD) and not _wmo_render_build_queue.is_empty():
var unique_key := String(_wmo_render_build_queue.front())
if not _wmo_render_build_jobs.has(unique_key):
_wmo_render_build_queue.pop_front()
@@ -3665,7 +3652,7 @@ func _process_wmo_render_build_jobs() -> void:
_set_editor_owner_recursive(mesh_instance)
job["mesh_index"] = mesh_index + 1
_wmo_render_build_jobs[unique_key] = job
ops_left -= 1
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_RENDER_GROUP_BUILD)
continue
var multimeshes: Array = render_resource.get("multimeshes")
@@ -3697,7 +3684,7 @@ func _process_wmo_render_build_jobs() -> void:
_set_editor_owner_recursive(multimesh_instance)
job["multimesh_index"] = multimesh_index + 1
_wmo_render_build_jobs[unique_key] = job
ops_left -= 1
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_RENDER_GROUP_BUILD)
continue
_cancel_wmo_render_build_job(unique_key)
@@ -4209,16 +4196,14 @@ func _drain_m2_animation_loads() -> void:
_m2_animation_load_requests.erase(normalized_rel)
_m2_animation_finalize_queue.append(pending)
var ops_left: int = maxi(0, m2_animation_finalize_ops_per_tick)
while ops_left > 0 and not _m2_animation_finalize_queue.is_empty():
while not _m2_animation_finalize_queue.is_empty() and _render_budget_scheduler.try_consume_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE):
var pending: Dictionary = _m2_animation_finalize_queue.pop_front()
var normalized_rel := String(pending.get("normalized", ""))
if normalized_rel.is_empty() or _m2_animated_scene_cache.has(normalized_rel) or _m2_static_animation_cache.has(normalized_rel):
ops_left -= 1
continue
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED:
_m2_static_animation_cache[normalized_rel] = true
ops_left -= 1
continue
var path := String(pending.get("path", ""))
@@ -4232,11 +4217,9 @@ func _drain_m2_animation_loads() -> void:
_m2_animated_scene_cache[normalized_rel] = node as Node3D
if debug_streaming:
print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [normalized_rel, path, players.size()])
ops_left -= 1
continue
node.free()
_m2_static_animation_cache[normalized_rel] = true
ops_left -= 1
func _drain_m2_mesh_loads() -> void:
@@ -4255,16 +4238,14 @@ func _drain_m2_mesh_loads() -> void:
_m2_mesh_load_requests.erase(normalized_rel)
_m2_mesh_finalize_queue.append(pending)
var ops_left: int = maxi(0, m2_mesh_finalize_ops_per_tick)
while ops_left > 0 and not _m2_mesh_finalize_queue.is_empty():
while not _m2_mesh_finalize_queue.is_empty() and _render_budget_scheduler.try_consume_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_MESH_FINALIZE):
var pending: Dictionary = _m2_mesh_finalize_queue.pop_front()
var normalized_rel := String(pending.get("normalized", ""))
if normalized_rel.is_empty() or _m2_mesh_cache.has(normalized_rel):
ops_left -= 1
continue
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED:
_m2_missing_cache[normalized_rel] = true
ops_left -= 1
continue
var path := String(pending.get("path", ""))
@@ -4274,12 +4255,11 @@ func _drain_m2_mesh_loads() -> void:
_m2_mesh_cache[normalized_rel] = _prepare_m2_mesh_for_runtime(normalized_rel, mesh)
else:
_m2_missing_cache[normalized_rel] = true
ops_left -= 1
func _process_m2_build_jobs() -> void:
var ops_left: int = maxi(0, m2_build_groups_per_tick)
while ops_left > 0 and not _m2_build_queue.is_empty():
while _render_budget_scheduler.has_remaining_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_BUILD) and not _m2_build_queue.is_empty():
var key: String = String(_m2_build_queue.front())
if not _m2_build_jobs.has(key):
_m2_build_queue.pop_front()
@@ -4325,7 +4305,7 @@ func _process_m2_build_jobs() -> void:
if enable_m2_animated_instances and _m2_animation_load_requests.has(normalized_rel):
_m2_build_queue.pop_front()
_m2_build_queue.append(key)
ops_left -= 1
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.M2_BUILD)
continue
var batch_size: int = (
maxi(1, m2_animated_instances_per_tick)
@@ -4344,7 +4324,7 @@ func _process_m2_build_jobs() -> void:
if not _m2_missing_cache.has(normalized_rel):
_m2_build_queue.pop_front()
_m2_build_queue.append(key)
ops_left -= 1
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.M2_BUILD)
continue
else:
_materialize_m2_group_batch(root as Node3D, rel_path, mesh, transforms, offset, batch_count, serial)
@@ -4355,7 +4335,7 @@ func _process_m2_build_jobs() -> void:
else:
job["offset"] = offset + consumed
_m2_build_jobs[key] = job
ops_left -= 1
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.M2_BUILD)
if int(job["index"]) >= group_keys.size():
_finish_m2_build_job(key)
+57
View File
@@ -0,0 +1,57 @@
{
"schema_version": 1,
"profile_id": "Blizzlike335",
"sources": {
"original_client_default_bindings": {
"status": "verified_private_metadata",
"build": 12340,
"path": "WTF/DefaultBindings.wtf",
"sha256": "35FFA0E4E2356A13A0633A848F9E57ABE7DF9D8A16C2245E15D33013AD2F56A5",
"distribution": "Selected binding facts only; proprietary file remains outside Git."
},
"original_client_binding_commands": {
"status": "verified_private_metadata",
"build": 12340,
"path": "Interface/FrameXML/Bindings.xml",
"sha256": "2E01276AFB7462F1417710F13B4BF1A456341F7F28CC8AE0D6D9C7442D7F6793",
"distribution": "Hash and selected command names only; proprietary file remains outside Git."
},
"trinitycore_player_turn_rate": {
"status": "verified_public_source",
"repository": "TrinityCore/TrinityCore",
"revision": "2853a621d6af91a803787a2b8a509f4ce3e0300d",
"path": "src/server/game/Entities/Unit/Unit.cpp",
"url": "https://raw.githubusercontent.com/TrinityCore/TrinityCore/2853a621d6af91a803787a2b8a509f4ce3e0300d/src/server/game/Entities/Unit/Unit.cpp",
"symbol": "playerBaseMoveSpeed[MOVE_TURN_RATE]",
"radians_per_second": 3.141594
},
"wowee_mouse_strafe_reference": {
"status": "verified_public_reference_only",
"repository": "Kelsidavis/WoWee",
"revision": "626243e937fb93965fa583a6507ed5a1aa7dda4b",
"path": "src/rendering/camera_controller.cpp",
"sha256": "54A41EFDA77629B2EAB432734A94D0F6F58223F3E4752338FF4C321E7835001B",
"observation": "A/D turn while right mouse is released and strafe while it is held; Q/E always strafe.",
"fidelity_limit": "Reference implementation behavior, not original-client proof."
}
},
"selected_default_bindings": [
{"command": "MOVEFORWARD", "keys": ["W", "UP"], "implemented_action": "openwc_player_move_forward"},
{"command": "MOVEBACKWARD", "keys": ["S", "DOWN"], "implemented_action": "openwc_player_move_backward"},
{"command": "TURNLEFT", "keys": ["A", "LEFT"], "implemented_action": "openwc_player_turn_left"},
{"command": "TURNRIGHT", "keys": ["D", "RIGHT"], "implemented_action": "openwc_player_turn_right"},
{"command": "STRAFELEFT", "keys": ["Q"], "implemented_action": "openwc_player_blizzlike_strafe_left"},
{"command": "STRAFERIGHT", "keys": ["E"], "implemented_action": "openwc_player_blizzlike_strafe_right"},
{"command": "JUMP", "keys": ["SPACE", "NUMPAD0"], "implemented_action": null},
{"command": "TOGGLEAUTORUN", "keys": ["NUMLOCK", "BUTTON4"], "implemented_action": null},
{"command": "CAMERAZOOMIN", "keys": ["MOUSEWHEELUP"], "implemented_action": "openwc_player_camera_zoom_in"},
{"command": "CAMERAZOOMOUT", "keys": ["MOUSEWHEELDOWN"], "implemented_action": "openwc_player_camera_zoom_out"},
{"command": "TURNORACTION", "keys": ["BUTTON2"], "implemented_action": "openwc_player_camera_rotate"},
{"command": "CAMERAORSELECTORMOVE", "keys": ["BUTTON1"], "implemented_action": null}
],
"known_gaps": [
"Jump, autorun and left-button camera/select behavior are recorded but not implemented by this package.",
"The server-core turn-rate default is compatibility evidence, not direct original-client timing proof.",
"Right-mouse A/D strafe routing is supported by a public reference implementation, not direct original-client capture."
]
}
+11 -7
View File
@@ -64,7 +64,12 @@ func _capture_async() -> void:
camera.far = 50000.0
camera.position = _vector3(first.get("camera", [0.0, 0.0, 0.0]))
(world as Node3D).add_child(camera)
world.set("streaming_focus_source_path", NodePath("CheckpointCamera"))
var render_facade := world.get_node_or_null("WorldRenderFacade")
if render_facade == null:
push_error("Streaming scene has no WorldRenderFacade")
quit(1)
return
render_facade.set("streaming_focus_source_path", NodePath("../CheckpointCamera"))
world.set("debug_streaming", true)
world.set("runtime_stats_enabled", true)
get_root().add_child(world)
@@ -126,8 +131,7 @@ func _capture_async() -> void:
if player != null:
player.global_position = _vector3(checkpoint.get("player", checkpoint.get("target", [0.0, 0.0, 0.0])))
_set_sky_time(world, float(checkpoint.get("time_hours", 13.0)))
if world.has_method("refresh_streaming_focus"):
world.call("refresh_streaming_focus", true)
render_facade.call("refresh_streaming_focus", true)
if dry_run:
print("RENDER_CHECKPOINT dry_run name=%s coverage=%s camera=%s target=%s yaw_offset=%.2f pitch_offset=%.2f time=%.2f" % [
@@ -148,7 +152,7 @@ func _capture_async() -> void:
var load_started := Time.get_ticks_usec()
await create_timer(maxf(wait_seconds, 0.0)).timeout
var load_time_ms := float(Time.get_ticks_usec() - load_started) / 1000.0
var metrics := await _measure_frames(maxf(measure_seconds, 0.1), world)
var metrics := await _measure_frames(maxf(measure_seconds, 0.1), render_facade)
await RenderingServer.frame_post_draw
var image := get_root().get_texture().get_image()
@@ -189,7 +193,7 @@ func _capture_async() -> void:
quit(0)
func _measure_frames(seconds: float, world: Node) -> Dictionary:
func _measure_frames(seconds: float, render_facade: Node) -> Dictionary:
var frame_times: Array[float] = []
var deadline := Time.get_ticks_usec() + int(seconds * 1000000.0)
var previous := Time.get_ticks_usec()
@@ -200,8 +204,8 @@ func _measure_frames(seconds: float, world: Node) -> Dictionary:
previous = now
frame_times.sort()
var snapshot := {}
if world.has_method("render_baseline_snapshot"):
snapshot = world.call("render_baseline_snapshot")
if render_facade.has_method("renderer_metrics_snapshot"):
snapshot = render_facade.call("renderer_metrics_snapshot")
return {
"frames": frame_times.size(),
"frame_ms_p50": _percentile(frame_times, 0.50),
@@ -0,0 +1 @@
uid://b5n7erahq8i46
+7 -3
View File
@@ -28,7 +28,12 @@ func _run_async() -> void:
camera.name = "OccluderProbeCamera"
camera.current = true
world.add_child(camera)
world.set("streaming_focus_source_path", NodePath(camera.name))
var render_facade := world.get_node_or_null("WorldRenderFacade")
if render_facade == null:
push_error("CAMERA_OCCLUDER_PROBE: streaming scene has no WorldRenderFacade")
quit(1)
return
render_facade.set("streaming_focus_source_path", NodePath("../%s" % camera.name))
world.set("debug_streaming", false)
get_root().add_child(world)
await process_frame
@@ -47,8 +52,7 @@ func _run_async() -> void:
var camera_position := _vector3(checkpoint.get("camera", []))
var target_position := _vector3(checkpoint.get("target", []))
camera.global_position = camera_position
if world.has_method("refresh_streaming_focus"):
world.call("refresh_streaming_focus", true)
render_facade.call("refresh_streaming_focus", true)
await create_timer(maxf(0.1, wait_seconds)).timeout
var geometry_nodes: Array[Node3D] = []
_collect_geometry_nodes(world, geometry_nodes)
@@ -0,0 +1 @@
uid://bpdv66fxxslbc
+7 -3
View File
@@ -37,7 +37,12 @@ func _run_async() -> void:
var camera := Camera3D.new()
camera.current = true
world.add_child(camera)
world.set("streaming_focus_source_path", NodePath(camera.name))
var render_facade := world.get_node_or_null("WorldRenderFacade")
if render_facade == null:
push_error("TERRAIN_HEIGHT_PROBE: streaming scene has no WorldRenderFacade")
quit(1)
return
render_facade.set("streaming_focus_source_path", NodePath("../%s" % camera.name))
world.set("debug_streaming", false)
get_root().add_child(world)
await process_frame
@@ -54,8 +59,7 @@ func _run_async() -> void:
continue
var camera_position := _vector3(checkpoint.get("camera", []))
camera.global_position = camera_position
if world.has_method("refresh_streaming_focus"):
world.call("refresh_streaming_focus", true)
render_facade.call("refresh_streaming_focus", true)
await create_timer(maxf(0.1, wait_seconds)).timeout
var terrain_sample := _sample_terrain(world, camera_position)
terrain_sample["name"] = checkpoint.get("name", "checkpoint")
@@ -0,0 +1 @@
uid://dctxeqgyptl8l
@@ -0,0 +1 @@
uid://buw0m8pjm5h02
@@ -19,7 +19,8 @@ const ALLOWED_LEGACY_NAME_PATHS: Array[String] = [
]
const REQUIRED_MAPPER_CALLS := {
"res://src/scenes/sky/wow_sky_controller.gd": ["godot_to_canonical", "godot_to_adt_tile", "godot_to_adt_tile_local"],
"res://src/scenes/streaming/streaming_world_loader.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local", "adt_tile_local_to_godot"],
"res://src/scenes/streaming/streaming_world_loader.gd": ["godot_to_adt_tile", "adt_tile_local_to_godot"],
"res://src/render/streaming/streaming_target_planner.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local"],
"res://src/scenes/player/third_person_wow_controller.gd": ["adt_tile_local_to_godot"],
"res://src/gameplay/terrain/adt_terrain_query.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local", "adt_tile_local_to_chunk"],
"res://src/tools/probe_render_terrain_height.gd": ["godot_to_adt_tile"],
@@ -60,7 +61,7 @@ func _initialize() -> void:
quit(1)
return
print("COORDINATE_CONVERSION_BOUNDARIES PASS files=%d native_boundary=1 oracle=1 consumers=5 classifier_cases=6" % source_paths.size())
print("COORDINATE_CONVERSION_BOUNDARIES PASS files=%d native_boundary=1 oracle=1 consumers=6 classifier_cases=6" % source_paths.size())
quit(0)
+26 -4
View File
@@ -12,6 +12,7 @@ func _initialize() -> void:
_verify_acceleration_and_deceleration(failures)
_verify_sandbox_sprint(failures)
_verify_flight_state_and_basis(failures)
_verify_keyboard_turn(failures)
_verify_invalid_delta(failures)
_verify_scene_boundary(failures)
@@ -21,7 +22,7 @@ func _initialize() -> void:
quit(1)
return
print("LOCAL_PLAYER_MOVEMENT PASS cases=12 state_transitions=2 scene_boundary=1")
print("LOCAL_PLAYER_MOVEMENT PASS cases=15 state_transitions=2 scene_boundary=1")
quit(0)
@@ -88,6 +89,13 @@ func _verify_invalid_delta(failures: Array[String]) -> void:
_expect_vector_near(controller.godot_world_velocity_units_per_second, Vector3.ZERO, "negative delta state", failures)
func _verify_keyboard_turn(failures: Array[String]) -> void:
var controller := _new_controller()
_expect_near(controller.calculate_yaw_delta_radians(_intent(0.0, 0.0, 0.0, false, 1.0), 0.5), -PI * 0.5, "right turn yaw", failures)
_expect_near(controller.calculate_yaw_delta_radians(_intent(0.0, 0.0, 0.0, false, -1.0), 0.5), PI * 0.5, "left turn yaw", failures)
_expect_near(controller.calculate_yaw_delta_radians(_intent(0.0, 0.0, 0.0, false, 1.0), -1.0), 0.0, "negative turn delta", failures)
func _verify_scene_boundary(failures: Array[String]) -> void:
var movement_source := _read_text(MOVEMENT_CONTROLLER_PATH, failures)
for forbidden_text in ["extends Node", "extends Resource", "Input.", "Camera3D", "ADTLoader", "global_position"]:
@@ -102,16 +110,25 @@ func _verify_scene_boundary(failures: Array[String]) -> void:
func _new_controller() -> LocalPlayerMovementController:
return LocalPlayerMovementController.new(7.0, 4.5, 4.5, 7.0, 28.0, 6.0)
return LocalPlayerMovementController.new(
7.0,
4.5,
4.5,
7.0,
28.0,
6.0,
PlayerMovementCapabilities.render_sandbox()
)
func _intent(
forward_axis: float,
strafe_axis: float,
vertical_axis: float = 0.0,
sprint_requested: bool = false
sprint_requested: bool = false,
turn_axis: float = 0.0
) -> MoveIntent:
return MoveIntent.new(forward_axis, strafe_axis, vertical_axis, sprint_requested)
return MoveIntent.new(forward_axis, strafe_axis, vertical_axis, sprint_requested, turn_axis)
func _read_text(path: String, failures: Array[String]) -> String:
@@ -132,6 +149,11 @@ func _expect_vector_near(
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 absf(actual_value - expected_value) > 0.000001:
failures.append("%s expected %.6f, got %.6f" % [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://qpkd87g1w31j
+82 -1
View File
@@ -6,6 +6,7 @@ const MOVE_INTENT_PATH := "res://src/domain/input/move_intent.gd"
const PLAYER_INPUT_SOURCE_PATH := "res://src/gameplay/input/player_input_source.gd"
const PLAYER_CONTROLLER_PATH := "res://src/scenes/player/third_person_wow_controller.gd"
const REGRESSION_SCENE_PATH := "res://src/tests/scenes/player_input_regression.tscn"
const PROFILE_FIXTURE_PATH := "res://src/tests/fixtures/player_input_profile_335.json"
func _initialize() -> void:
@@ -15,6 +16,7 @@ func _initialize() -> void:
func _run_verification() -> void:
var failures: Array[String] = []
_verify_default_input_actions(failures)
_verify_profile_fixture(failures)
_verify_move_intent_composition(failures)
_verify_controller_boundary(failures)
_verify_regression_scene(failures)
@@ -25,7 +27,7 @@ func _run_verification() -> void:
quit(1)
return
print("PLAYER_INPUT PASS actions=%d intent_cases=6 controller=1 regression_scene=1" % PlayerInputActions.REQUIRED_ACTIONS.size())
print("PLAYER_INPUT PASS actions=%d intent_cases=9 profiles=3 fixture=1 controller=1 regression_scene=3" % PlayerInputActions.REQUIRED_ACTIONS.size())
quit(0)
@@ -34,9 +36,17 @@ func _verify_default_input_actions(failures: Array[String]) -> void:
_expect_true(InputMap.has_action(action_name), "Input Map contains %s" % action_name, failures)
_expect_key_binding(PlayerInputActions.MOVE_FORWARD, KEY_W, true, failures)
_expect_key_binding(PlayerInputActions.MOVE_FORWARD, KEY_UP, false, failures)
_expect_key_binding(PlayerInputActions.MOVE_BACKWARD, KEY_S, true, failures)
_expect_key_binding(PlayerInputActions.MOVE_BACKWARD, KEY_DOWN, false, failures)
_expect_key_binding(PlayerInputActions.STRAFE_LEFT, KEY_A, true, failures)
_expect_key_binding(PlayerInputActions.STRAFE_RIGHT, KEY_D, true, failures)
_expect_key_binding(PlayerInputActions.BLIZZLIKE_STRAFE_LEFT, KEY_Q, true, failures)
_expect_key_binding(PlayerInputActions.BLIZZLIKE_STRAFE_RIGHT, KEY_E, true, failures)
_expect_key_binding(PlayerInputActions.TURN_LEFT, KEY_A, true, failures)
_expect_key_binding(PlayerInputActions.TURN_LEFT, KEY_LEFT, false, failures)
_expect_key_binding(PlayerInputActions.TURN_RIGHT, KEY_D, true, failures)
_expect_key_binding(PlayerInputActions.TURN_RIGHT, KEY_RIGHT, false, failures)
_expect_key_binding(PlayerInputActions.DEBUG_FLY_UP, KEY_E, true, failures)
_expect_key_binding(PlayerInputActions.DEBUG_FLY_DOWN, KEY_Q, true, failures)
_expect_key_binding(PlayerInputActions.DEBUG_SPRINT, KEY_SHIFT, false, failures)
@@ -47,6 +57,40 @@ func _verify_default_input_actions(failures: Array[String]) -> void:
_expect_mouse_binding(PlayerInputActions.CAMERA_ZOOM_OUT, MOUSE_BUTTON_WHEEL_DOWN, failures)
func _verify_profile_fixture(failures: Array[String]) -> void:
var fixture_text := _read_text(PROFILE_FIXTURE_PATH, failures)
var parsed_fixture: Variant = JSON.parse_string(fixture_text)
_expect_true(parsed_fixture is Dictionary, "profile fixture parses", failures)
if not parsed_fixture is Dictionary:
return
var fixture := parsed_fixture as Dictionary
_expect_true(fixture.get("schema_version") == 1, "profile fixture schema", failures)
_expect_true(fixture.get("profile_id") == "Blizzlike335", "profile fixture identity", failures)
var sources := fixture.get("sources", {}) as Dictionary
var bindings_source := sources.get("original_client_default_bindings", {}) as Dictionary
_expect_true(bindings_source.get("build") == 12340, "fixture client build", failures)
_expect_true(
bindings_source.get("sha256") == "35FFA0E4E2356A13A0633A848F9E57ABE7DF9D8A16C2245E15D33013AD2F56A5",
"fixture binding source hash",
failures
)
var commands_source := sources.get("original_client_binding_commands", {}) as Dictionary
_expect_true(
commands_source.get("sha256") == "2E01276AFB7462F1417710F13B4BF1A456341F7F28CC8AE0D6D9C7442D7F6793",
"fixture command source hash",
failures
)
var turn_source := sources.get("trinitycore_player_turn_rate", {}) as Dictionary
_expect_near(float(turn_source.get("radians_per_second", 0.0)), 3.141594, "fixture turn rate", failures)
var mouse_strafe_source := sources.get("wowee_mouse_strafe_reference", {}) as Dictionary
_expect_true(
mouse_strafe_source.get("revision") == "626243e937fb93965fa583a6507ed5a1aa7dda4b",
"fixture mouse-strafe reference revision",
failures
)
_expect_true((fixture.get("selected_default_bindings", []) as Array).size() == 12, "fixture selected binding count", failures)
func _verify_move_intent_composition(failures: Array[String]) -> void:
var neutral := PlayerInputSource.compose_move_intent(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false)
_expect_near(neutral.forward_axis, 0.0, "neutral forward", failures)
@@ -67,6 +111,18 @@ func _verify_move_intent_composition(failures: Array[String]) -> void:
var debug_requests := PlayerInputSource.compose_move_intent(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, true)
_expect_true(debug_requests.is_sprint_requested, "sprint request retained", failures)
var blizzlike_strafe := PlayerInputSource.compose_blizzlike_335_move_intent(0.0, 0.0, 1.0, 0.0, 0.0, 0.0, false)
_expect_near(blizzlike_strafe.strafe_axis, -1.0, "blizzlike Q strafe", failures)
var blizzlike_turn := PlayerInputSource.compose_blizzlike_335_move_intent(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, false)
_expect_near(blizzlike_turn.turn_axis, 1.0, "blizzlike D turn", failures)
var mouse_strafe := PlayerInputSource.compose_blizzlike_335_move_intent(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, true)
_expect_near(mouse_strafe.strafe_axis, 1.0, "camera-held D strafe", failures)
_expect_near(mouse_strafe.turn_axis, 0.0, "camera-held D suppresses turn", failures)
_expect_true(PlayerInputSource.new(&"RenderSandbox").input_profile_id == &"RenderSandbox", "sandbox profile selected", failures)
_expect_true(PlayerInputSource.new(&"Blizzlike335").input_profile_id == &"Blizzlike335", "blizzlike profile selected", failures)
_expect_true(PlayerInputSource.new(&"Unknown").input_profile_id == &"Blizzlike335", "unknown profile fails closed", failures)
func _verify_controller_boundary(failures: Array[String]) -> void:
var controller_source := _read_text(PLAYER_CONTROLLER_PATH, failures)
@@ -118,6 +174,31 @@ func _verify_regression_scene(failures: Array[String]) -> void:
_expect_true(movement_controller.is_flight_enabled, "flight action preserves immediate sandbox toggle", failures)
player.free()
var blizzlike_player := packed_scene.instantiate() as CharacterBody3D
blizzlike_player.set("movement_profile_id", "Blizzlike335")
root.add_child(blizzlike_player)
blizzlike_player.set_physics_process(false)
Input.action_press(PlayerInputActions.TURN_RIGHT)
blizzlike_player.call("_physics_process", 0.5)
Input.action_release(PlayerInputActions.TURN_RIGHT)
_expect_near(blizzlike_player.rotation.y, -3.141594 * 0.5, "blizzlike D turns right", failures)
var blizzlike_camera := blizzlike_player.get_node("CameraPivot") as ThirdPersonCameraRig
_expect_near(blizzlike_camera.yaw_radians, blizzlike_player.rotation.y, "keyboard turn synchronizes camera yaw", failures)
blizzlike_player.free()
var mouse_strafe_player := packed_scene.instantiate() as CharacterBody3D
mouse_strafe_player.set("movement_profile_id", "Blizzlike335")
root.add_child(mouse_strafe_player)
mouse_strafe_player.set_physics_process(false)
Input.action_press(PlayerInputActions.CAMERA_ROTATE)
Input.action_press(PlayerInputActions.TURN_RIGHT)
mouse_strafe_player.call("_physics_process", 0.5)
Input.action_release(PlayerInputActions.TURN_RIGHT)
Input.action_release(PlayerInputActions.CAMERA_ROTATE)
_expect_near(mouse_strafe_player.rotation.y, 0.0, "camera-held D does not turn", failures)
_expect_near(mouse_strafe_player.position.x, 2.25, "camera-held D strafes right", failures)
mouse_strafe_player.free()
func _expect_key_binding(
action_name: StringName,
+1
View File
@@ -0,0 +1 @@
uid://dwx3m77yybv0h
@@ -0,0 +1,148 @@
extends SceneTree
## Headless M02 profile/capability gate regression for debug player movement.
const MOVEMENT_CAPABILITIES_PATH := "res://src/gameplay/movement/player_movement_capabilities.gd"
const MOVEMENT_CONTROLLER_PATH := "res://src/gameplay/movement/local_player_movement_controller.gd"
const PLAYER_CONTROLLER_PATH := "res://src/scenes/player/third_person_wow_controller.gd"
const REGRESSION_SCENE_PATH := "res://src/tests/scenes/player_input_regression.tscn"
func _initialize() -> void:
_run_verification.call_deferred()
func _run_verification() -> void:
var failures: Array[String] = []
_verify_capability_profiles(failures)
_verify_movement_gate(failures)
_verify_real_scene_profiles(failures)
_verify_source_boundary(failures)
if not failures.is_empty():
for failure in failures:
push_error("PLAYER_MOVEMENT_CAPABILITIES: %s" % failure)
quit(1)
return
print("PLAYER_MOVEMENT_CAPABILITIES PASS profiles=3 sprint=2 flight=2 scenes=2 boundary=1")
quit(0)
func _verify_capability_profiles(failures: Array[String]) -> void:
var render_sandbox := PlayerMovementCapabilities.render_sandbox()
_expect_true(render_sandbox.profile_id == PlayerMovementCapabilities.RENDER_SANDBOX_PROFILE_ID, "sandbox profile identity", failures)
_expect_true(render_sandbox.allows_debug_sprint, "sandbox sprint capability", failures)
_expect_true(render_sandbox.allows_debug_free_flight, "sandbox flight capability", failures)
var blizzlike := PlayerMovementCapabilities.blizzlike_335()
_expect_true(blizzlike.profile_id == PlayerMovementCapabilities.BLIZZLIKE_335_PROFILE_ID, "blizzlike profile identity", failures)
_expect_true(not blizzlike.allows_debug_sprint, "blizzlike rejects sprint", failures)
_expect_true(not blizzlike.allows_debug_free_flight, "blizzlike rejects flight", failures)
var unknown_profile := PlayerMovementCapabilities.for_profile_id(&"UnknownProfile")
_expect_true(unknown_profile.profile_id == PlayerMovementCapabilities.BLIZZLIKE_335_PROFILE_ID, "unknown profile fails closed", failures)
_expect_true(not unknown_profile.allows_debug_sprint, "unknown profile rejects sprint", failures)
_expect_true(not unknown_profile.allows_debug_free_flight, "unknown profile rejects flight", failures)
func _verify_movement_gate(failures: Array[String]) -> void:
var sprint_intent := MoveIntent.new(1.0, 0.0, 0.0, true)
var sandbox_controller := _new_controller(PlayerMovementCapabilities.render_sandbox())
sandbox_controller.advance(sprint_intent, Basis.IDENTITY, 2.0)
_expect_vector_near(sandbox_controller.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -42.0), "sandbox sprint applied", failures)
_expect_true(sandbox_controller.toggle_sandbox_flight(), "sandbox flight enabled", failures)
var blizzlike_controller := _new_controller(PlayerMovementCapabilities.blizzlike_335())
blizzlike_controller.advance(sprint_intent, Basis.IDENTITY, 2.0)
_expect_vector_near(blizzlike_controller.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -7.0), "blizzlike sprint ignored", failures)
_expect_true(not blizzlike_controller.toggle_sandbox_flight(), "blizzlike flight rejected", failures)
_expect_true(not blizzlike_controller.is_flight_enabled, "blizzlike remains grounded", failures)
var safe_default_controller := LocalPlayerMovementController.new(7.0, 4.5, 4.5, 7.0, 28.0, 6.0)
_expect_true(safe_default_controller.movement_profile_id == PlayerMovementCapabilities.BLIZZLIKE_335_PROFILE_ID, "missing capability defaults safe", failures)
func _verify_real_scene_profiles(failures: Array[String]) -> void:
var sandbox_player := _instantiate_player("RenderSandbox", failures)
if sandbox_player != null:
_press_sprint_forward_for_two_seconds(sandbox_player)
var sandbox_movement: LocalPlayerMovementController = sandbox_player.get("_local_movement_controller")
_expect_vector_near(sandbox_movement.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -42.0), "sandbox scene sprint", failures)
sandbox_player.call("_unhandled_input", _action_event(PlayerInputActions.DEBUG_TOGGLE_FLIGHT))
_expect_true(sandbox_movement.is_flight_enabled, "sandbox scene flight", failures)
sandbox_player.free()
var blizzlike_player := _instantiate_player("Blizzlike335", failures)
if blizzlike_player != null:
_press_sprint_forward_for_two_seconds(blizzlike_player)
var blizzlike_movement: LocalPlayerMovementController = blizzlike_player.get("_local_movement_controller")
_expect_vector_near(blizzlike_movement.godot_world_velocity_units_per_second, Vector3(0.0, 0.0, -7.0), "blizzlike scene sprint rejected", failures)
blizzlike_player.call("_unhandled_input", _action_event(PlayerInputActions.DEBUG_TOGGLE_FLIGHT))
_expect_true(not blizzlike_movement.is_flight_enabled, "blizzlike scene flight rejected", failures)
blizzlike_player.free()
func _instantiate_player(profile_id: String, failures: Array[String]) -> CharacterBody3D:
var packed_scene := load(REGRESSION_SCENE_PATH) as PackedScene
if packed_scene == null:
failures.append("regression scene loads")
return null
var player := packed_scene.instantiate() as CharacterBody3D
if player == null:
failures.append("regression player instantiates")
return null
player.set("movement_profile_id", profile_id)
root.add_child(player)
player.set_physics_process(false)
return player
func _press_sprint_forward_for_two_seconds(player: CharacterBody3D) -> void:
Input.action_press(PlayerInputActions.MOVE_FORWARD)
Input.action_press(PlayerInputActions.DEBUG_SPRINT)
player.call("_physics_process", 2.0)
Input.action_release(PlayerInputActions.DEBUG_SPRINT)
Input.action_release(PlayerInputActions.MOVE_FORWARD)
func _action_event(action_name: StringName) -> InputEventAction:
var input_event := InputEventAction.new()
input_event.action = action_name
input_event.pressed = true
return input_event
func _verify_source_boundary(failures: Array[String]) -> void:
var capability_source := _read_text(MOVEMENT_CAPABILITIES_PATH, failures)
for forbidden_text in ["extends Node", "extends Resource", "Input.", "CharacterBody3D"]:
_expect_true(not capability_source.contains(forbidden_text), "capability omits %s" % forbidden_text, failures)
_expect_true(not capability_source.contains("\n\tset:"), "capability exposes no property setters", failures)
var movement_source := _read_text(MOVEMENT_CONTROLLER_PATH, failures)
_expect_true(movement_source.contains("allows_debug_sprint"), "movement gates sprint capability", failures)
_expect_true(movement_source.contains("allows_debug_free_flight"), "movement gates flight capability", failures)
var player_source := _read_text(PLAYER_CONTROLLER_PATH, failures)
_expect_true(player_source.contains("for_profile_id(StringName(movement_profile_id))"), "scene maps profile once", failures)
_expect_true(not player_source.contains('movement_profile_id == "'), "scene omits profile branches", failures)
func _new_controller(capabilities: PlayerMovementCapabilities) -> LocalPlayerMovementController:
return LocalPlayerMovementController.new(7.0, 4.5, 4.5, 7.0, 28.0, 6.0, capabilities)
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_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_true(actual_value: bool, label: String, failures: Array[String]) -> void:
if not actual_value:
failures.append("%s expected true" % label)
@@ -0,0 +1 @@
uid://bcg11uv05uhcr
@@ -0,0 +1 @@
uid://bopeup4jipa35
+175
View File
@@ -0,0 +1,175 @@
extends SceneTree
## Asset-free M03 regression for bounded renderer operation permits and cancellation.
const SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
const SCHEDULER_PATH := "res://src/render/streaming/render_budget_scheduler.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
const PERFORMANCE_ITERATIONS := 20000
const MAXIMUM_PERMIT_CHECK_MILLISECONDS := 150.0
func _initialize() -> void:
var failures: Array[String] = []
_verify_bounded_lane_and_diagnostics(failures)
_verify_shared_lane_priority(failures)
_verify_independent_lanes_and_frame_reset(failures)
_verify_invalid_limits_and_unknown_lane(failures)
_verify_cancellation(failures)
_verify_scene_free_extraction(failures)
var elapsed_milliseconds := _measure_permit_checks(failures)
if not failures.is_empty():
for failure in failures:
push_error("RENDER_BUDGET_SCHEDULER: %s" % failure)
quit(1)
return
print("RENDER_BUDGET_SCHEDULER PASS cases=6 iterations=%d elapsed_ms=%.3f" % [
PERFORMANCE_ITERATIONS,
elapsed_milliseconds,
])
quit(0)
func _verify_bounded_lane_and_diagnostics(failures: Array[String]) -> void:
var scheduler = SCHEDULER_SCRIPT.new()
scheduler.begin_frame({SCHEDULER_SCRIPT.TILE_FINALIZE: 2})
_expect_true(scheduler.try_consume_permit(SCHEDULER_SCRIPT.TILE_FINALIZE), "first permit", failures)
_expect_true(scheduler.try_consume_permit(SCHEDULER_SCRIPT.TILE_FINALIZE), "second permit", failures)
_expect_false(scheduler.try_consume_permit(SCHEDULER_SCRIPT.TILE_FINALIZE), "exhausted permit", failures)
_expect_equal(scheduler.remaining_permits(SCHEDULER_SCRIPT.TILE_FINALIZE), 0, "remaining permits", failures)
var consumed_snapshot: Dictionary = scheduler.consumed_permits_snapshot()
_expect_equal(int(consumed_snapshot[SCHEDULER_SCRIPT.TILE_FINALIZE]), 2, "consumed permits", failures)
consumed_snapshot[SCHEDULER_SCRIPT.TILE_FINALIZE] = 99
_expect_equal(
int(scheduler.consumed_permits_snapshot()[SCHEDULER_SCRIPT.TILE_FINALIZE]),
2,
"diagnostic snapshot is detached",
failures
)
func _verify_shared_lane_priority(failures: Array[String]) -> void:
var scheduler = SCHEDULER_SCRIPT.new()
scheduler.begin_frame({SCHEDULER_SCRIPT.CHUNK_GEOMETRY: 3})
var removals := 0
while removals < 2 and scheduler.try_consume_permit(SCHEDULER_SCRIPT.CHUNK_GEOMETRY):
removals += 1
var creates := 0
while scheduler.try_consume_permit(SCHEDULER_SCRIPT.CHUNK_GEOMETRY):
creates += 1
_expect_equal(removals, 2, "shared lane removals consume first", failures)
_expect_equal(creates, 1, "shared lane leaves one create", failures)
func _verify_independent_lanes_and_frame_reset(failures: Array[String]) -> void:
var scheduler = SCHEDULER_SCRIPT.new()
scheduler.begin_frame({
SCHEDULER_SCRIPT.TILE_LOD_CREATE: 1,
SCHEDULER_SCRIPT.TILE_LOD_REMOVE: 2,
})
_expect_true(scheduler.try_consume_permit(SCHEDULER_SCRIPT.TILE_LOD_CREATE), "create lane permit", failures)
_expect_equal(scheduler.remaining_permits(SCHEDULER_SCRIPT.TILE_LOD_REMOVE), 2, "remove lane independent", failures)
scheduler.begin_frame({SCHEDULER_SCRIPT.TILE_LOD_CREATE: 3})
_expect_equal(scheduler.remaining_permits(SCHEDULER_SCRIPT.TILE_LOD_CREATE), 3, "new frame resets lane", failures)
_expect_equal(scheduler.remaining_permits(SCHEDULER_SCRIPT.TILE_LOD_REMOVE), 0, "new frame removes absent lane", failures)
_expect_equal(int(scheduler.consumed_permits_snapshot()[SCHEDULER_SCRIPT.TILE_LOD_CREATE]), 0, "new frame resets diagnostics", failures)
func _verify_invalid_limits_and_unknown_lane(failures: Array[String]) -> void:
var scheduler = SCHEDULER_SCRIPT.new()
scheduler.begin_frame({
SCHEDULER_SCRIPT.WATER_FINALIZE: 0,
SCHEDULER_SCRIPT.M2_BUILD: -4,
})
_expect_false(scheduler.has_remaining_permit(SCHEDULER_SCRIPT.WATER_FINALIZE), "zero limit", failures)
_expect_false(scheduler.try_consume_permit(SCHEDULER_SCRIPT.M2_BUILD), "negative limit clamps", failures)
_expect_false(scheduler.try_consume_permit(&"unknown_lane"), "unknown lane", failures)
func _verify_cancellation(failures: Array[String]) -> void:
var scheduler = SCHEDULER_SCRIPT.new()
scheduler.begin_frame({SCHEDULER_SCRIPT.WMO_BUILD: 2})
scheduler.cancel()
_expect_true(scheduler.is_cancelled(), "cancelled state", failures)
_expect_false(scheduler.try_consume_permit(SCHEDULER_SCRIPT.WMO_BUILD), "cancel rejects permit", failures)
scheduler.begin_frame({SCHEDULER_SCRIPT.WMO_BUILD: 5})
_expect_equal(scheduler.remaining_permits(SCHEDULER_SCRIPT.WMO_BUILD), 0, "begin frame cannot revive cancelled lifecycle", failures)
func _verify_scene_free_extraction(failures: Array[String]) -> void:
var scheduler_source := _read_text(SCHEDULER_PATH, failures)
_expect_true(scheduler_source.contains("extends RefCounted"), "scheduler is RefCounted", failures)
for forbidden_text in ["extends Node", "WorkerThreadPool", "ResourceLoader", "RenderingServer", "Mutex"]:
_expect_false(scheduler_source.contains(forbidden_text), "scheduler omits %s" % forbidden_text, failures)
var loader_source := _read_text(LOADER_PATH, failures)
_expect_true(loader_source.contains("begin_frame(_render_operation_limits_for_frame())"), "loader begins one budget frame", failures)
_expect_true(loader_source.contains("_render_budget_scheduler.cancel()"), "loader cancels scheduler on teardown", failures)
_expect_false(loader_source.contains("var ops_left"), "loader has no local operation counters", failures)
_expect_false(loader_source.contains("var finalize_budget"), "loader has no local finalize counter", failures)
for exported_limit_name in [
"tile_finalize_ops_per_tick",
"terrain_upgrade_finalize_ops_per_tick",
"terrain_control_splat_cache_finalize_ops_per_tick",
"terrain_splat_cache_finalize_ops_per_tick",
"terrain_splat_builds_per_tick",
"water_finalize_ops_per_tick",
"chunk_ops_per_tick",
"tiles_per_tick",
"tile_lod_ops_per_tick",
"tile_lod_remove_ops_per_tick",
"m2_animation_finalize_ops_per_tick",
"m2_mesh_finalize_ops_per_tick",
"m2_build_groups_per_tick",
"wmo_build_instances_per_tick",
"wmo_render_group_ops_per_tick",
"detail_asset_ops_per_tick",
]:
_expect_true(
loader_source.contains(": maxi(") and loader_source.contains(exported_limit_name),
"frame snapshot includes %s" % exported_limit_name,
failures
)
func _measure_permit_checks(failures: Array[String]) -> float:
var scheduler = SCHEDULER_SCRIPT.new()
scheduler.begin_frame({SCHEDULER_SCRIPT.DETAIL_ASSET_SYNC: PERFORMANCE_ITERATIONS})
var started_microseconds := Time.get_ticks_usec()
for _iteration in PERFORMANCE_ITERATIONS:
scheduler.try_consume_permit(SCHEDULER_SCRIPT.DETAIL_ASSET_SYNC)
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(
elapsed_milliseconds <= MAXIMUM_PERMIT_CHECK_MILLISECONDS,
"permit checks stay within %.1f ms (actual %.3f)" % [
MAXIMUM_PERMIT_CHECK_MILLISECONDS,
elapsed_milliseconds,
],
failures
)
return elapsed_milliseconds
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_equal(actual_value: int, expected_value: int, label: String, failures: Array[String]) -> void:
if actual_value != expected_value:
failures.append("%s expected %d, got %d" % [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)
func _expect_false(actual_value: bool, label: String, failures: Array[String]) -> void:
if actual_value:
failures.append("%s expected false" % label)
@@ -0,0 +1 @@
uid://b27nncxfog7cv
@@ -0,0 +1 @@
uid://cojh1u6igoocj
@@ -0,0 +1 @@
uid://cy5sbwxmjyue8
@@ -0,0 +1,164 @@
extends SceneTree
## M03 repository gate preventing gameplay/EditorPlugin coupling to streamer internals.
const STREAMING_WORLD_LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
const GAMEPLAY_ROOT := "res://src/gameplay"
const SOURCE_ROOTS: Array[String] = ["res://src", "res://addons"]
func _initialize() -> void:
var failures: Array[String] = []
var loader_source := _read_text(STREAMING_WORLD_LOADER_PATH, failures)
var private_renderer_symbols := _extract_private_renderer_symbols(loader_source, failures)
_verify_synthetic_detection(private_renderer_symbols, failures)
var gameplay_paths := _collect_gdscript_paths(GAMEPLAY_ROOT)
var editor_source_paths := _discover_editor_source_paths(failures)
var consumer_paths_by_path: Dictionary = {}
for gameplay_path in gameplay_paths:
consumer_paths_by_path[gameplay_path] = true
for editor_source_path in editor_source_paths:
consumer_paths_by_path[editor_source_path] = true
for consumer_path_variant in consumer_paths_by_path:
var consumer_path := String(consumer_path_variant)
var consumer_source := _read_text(consumer_path, failures)
for forbidden_symbol in _find_forbidden_symbols(consumer_source, private_renderer_symbols):
failures.append("%s accesses private renderer symbol %s" % [consumer_path, forbidden_symbol])
if not failures.is_empty():
for failure in failures:
push_error("RENDERER_INTERNAL_ACCESS: %s" % failure)
quit(1)
return
print("RENDERER_INTERNAL_ACCESS PASS private_symbols=%d gameplay=%d editor_sources=%d" % [
private_renderer_symbols.size(),
gameplay_paths.size(),
editor_source_paths.size(),
])
quit(0)
func _extract_private_renderer_symbols(loader_source: String, failures: Array[String]) -> Array[String]:
var declaration_pattern := RegEx.new()
var compile_error := declaration_pattern.compile(
"(?m)^var\\s+(_[A-Za-z0-9_]*(?:queue|tasks|cache|states)[A-Za-z0-9_]*)"
)
if compile_error != OK:
failures.append("cannot compile private renderer symbol pattern")
return []
var symbols_by_name: Dictionary = {}
for regex_match in declaration_pattern.search_all(loader_source):
symbols_by_name[regex_match.get_string(1)] = true
var symbols: Array[String] = []
for symbol_variant in symbols_by_name:
symbols.append(String(symbol_variant))
symbols.sort()
if symbols.is_empty():
failures.append("streamer private symbol inventory is empty")
return symbols
func _discover_editor_source_paths(failures: Array[String]) -> Array[String]:
var all_source_paths: Array[String] = []
for source_root in SOURCE_ROOTS:
all_source_paths.append_array(_collect_gdscript_paths(source_root))
var editor_roots_by_path: Dictionary = {}
var editor_source_paths_by_path: Dictionary = {}
for source_path in all_source_paths:
var source := _read_text(source_path, failures)
if source_path.contains("/editor/"):
editor_source_paths_by_path[source_path] = true
if source.contains("extends " + "EditorPlugin") or source_path.ends_with("/plugin.gd"):
editor_roots_by_path[source_path.get_base_dir()] = true
for source_path in all_source_paths:
for editor_root_variant in editor_roots_by_path:
var editor_root := String(editor_root_variant)
if source_path.begins_with(editor_root + "/"):
editor_source_paths_by_path[source_path] = true
var editor_source_paths: Array[String] = []
for editor_source_path_variant in editor_source_paths_by_path:
editor_source_paths.append(String(editor_source_path_variant))
editor_source_paths.sort()
return editor_source_paths
func _collect_gdscript_paths(root_path: String) -> Array[String]:
var paths: Array[String] = []
var directory := DirAccess.open(root_path)
if directory == null:
return paths
directory.list_dir_begin()
var entry_name := directory.get_next()
while not entry_name.is_empty():
if entry_name.begins_with(".") or entry_name == "reference":
entry_name = directory.get_next()
continue
var entry_path := root_path.path_join(entry_name)
if directory.current_is_dir():
paths.append_array(_collect_gdscript_paths(entry_path))
elif entry_name.ends_with(".gd"):
paths.append(entry_path)
entry_name = directory.get_next()
directory.list_dir_end()
paths.sort()
return paths
func _find_forbidden_symbols(source: String, private_renderer_symbols: Array[String]) -> Array[String]:
var found_symbols: Array[String] = []
for private_renderer_symbol in private_renderer_symbols:
if _contains_external_symbol_access(source, private_renderer_symbol):
found_symbols.append(private_renderer_symbol)
return found_symbols
func _contains_external_symbol_access(source: String, private_renderer_symbol: String) -> bool:
for access_text in [
"." + private_renderer_symbol,
'.get("%s"' % private_renderer_symbol,
".get('%s'" % private_renderer_symbol,
'.set("%s"' % private_renderer_symbol,
".set('%s'" % private_renderer_symbol,
'.call("%s"' % private_renderer_symbol,
".call('%s'" % private_renderer_symbol,
'["%s"]' % private_renderer_symbol,
"['%s']" % private_renderer_symbol,
]:
if source.contains(access_text):
return true
return false
func _verify_synthetic_detection(private_renderer_symbols: Array[String], failures: Array[String]) -> void:
if not private_renderer_symbols.has("_tile_states"):
failures.append("streamer inventory does not contain _tile_states fixture")
return
var forbidden_fixture := 'var tile_states = world.get("_tile_states")'
var detected_symbols := _find_forbidden_symbols(forbidden_fixture, private_renderer_symbols)
if detected_symbols != ["_tile_states"]:
failures.append("synthetic private access was not detected exactly")
var direct_member_fixture := "var tile_states = world._tile_states"
if _find_forbidden_symbols(direct_member_fixture, private_renderer_symbols) != ["_tile_states"]:
failures.append("synthetic direct member access was not detected exactly")
var unrelated_owner_fixture := "var _tile_states: Dictionary = {}"
if not _find_forbidden_symbols(unrelated_owner_fixture, private_renderer_symbols).is_empty():
failures.append("unrelated private declaration was rejected")
var safe_fixture := "var metrics = render_facade.renderer_metrics_snapshot()"
if not _find_forbidden_symbols(safe_fixture, private_renderer_symbols).is_empty():
failures.append("facade metrics fixture was rejected")
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()
@@ -0,0 +1 @@
uid://y11r5k0ve0md
+15 -3
View File
@@ -6,6 +6,7 @@ const StreamingFocusScript = preload("res://src/domain/streaming/streaming_focus
const GodotWorldPositionScript = preload("res://src/domain/coordinates/godot_world_position.gd")
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
const FACADE_PATH := "res://src/render/world_render_facade.gd"
const RUNTIME_SCENE_PATHS: Array[String] = [
"res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
"res://src/scenes/streaming/kalimdor_streaming.tscn",
@@ -21,6 +22,7 @@ func _initialize() -> void:
var failures: Array[String] = []
_verify_scene_free_focus_value(failures)
_verify_loader_boundary(failures)
_verify_facade_boundary(failures)
_verify_runtime_scene_wiring(failures)
_verify_capture_tool_wiring(failures)
@@ -65,11 +67,21 @@ func _verify_loader_boundary(failures: Array[String]) -> void:
_expect_true(not loader_source.contains(forbidden_text), "loader omits %s" % forbidden_text, failures)
func _verify_facade_boundary(failures: Array[String]) -> void:
var facade_source := _read_text(FACADE_PATH, failures)
for required_text in [
"class_name WorldRenderFacade",
"func set_streaming_focus(streaming_focus: StreamingFocus) -> void:",
"func refresh_streaming_focus(force: bool = false) -> bool:",
]:
_expect_true(facade_source.contains(required_text), "facade contains %s" % required_text, failures)
func _verify_runtime_scene_wiring(failures: Array[String]) -> void:
for scene_path in RUNTIME_SCENE_PATHS:
var scene_source := _read_text(scene_path, failures)
_expect_true(
scene_source.contains('streaming_focus_source_path = NodePath("ThirdPersonPlayer")'),
scene_source.contains('streaming_focus_source_path = NodePath("../ThirdPersonPlayer")'),
"%s uses player focus" % scene_path,
failures
)
@@ -78,8 +90,8 @@ func _verify_runtime_scene_wiring(failures: Array[String]) -> void:
func _verify_capture_tool_wiring(failures: Array[String]) -> void:
for tool_path in CAPTURE_TOOL_PATHS:
var tool_source := _read_text(tool_path, failures)
_expect_true(tool_source.contains('world.set("streaming_focus_source_path"'), "%s sets explicit focus source" % tool_path, failures)
_expect_true(tool_source.contains('world.call("refresh_streaming_focus", true)'), "%s uses public refresh" % tool_path, failures)
_expect_true(tool_source.contains('render_facade.set("streaming_focus_source_path"'), "%s sets explicit focus source" % tool_path, failures)
_expect_true(tool_source.contains('render_facade.call("refresh_streaming_focus", true)'), "%s uses facade refresh" % tool_path, failures)
_expect_true(not tool_source.contains('world.call("_refresh_streaming_targets_at"'), "%s avoids private refresh" % tool_path, failures)
@@ -0,0 +1,211 @@
extends SceneTree
## Asset-free M03 regression for pure runtime/editor ADT target planning.
const PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
const POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
const PLANNER_PATH := "res://src/render/streaming/streaming_target_planner.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
const PERFORMANCE_ITERATIONS := 250
const MAXIMUM_AVERAGE_PLAN_MILLISECONDS := 4.0
func _initialize() -> void:
var failures: Array[String] = []
var planner = PLANNER_SCRIPT.new()
_verify_center_radii_and_isolation(planner, failures)
_verify_corner_prefetch_and_catalog_filter(planner, failures)
_verify_policy_clamps(planner, failures)
_verify_editor_plan(planner, failures)
_verify_scene_free_extraction(failures)
var elapsed_milliseconds := _measure_planner_iterations(planner, failures)
if not failures.is_empty():
for failure in failures:
push_error("STREAMING_TARGET_PLANNER: %s" % failure)
quit(1)
return
print("STREAMING_TARGET_PLANNER PASS cases=5 iterations=%d elapsed_ms=%.3f average_ms=%.3f" % [
PERFORMANCE_ITERATIONS,
elapsed_milliseconds,
elapsed_milliseconds / float(PERFORMANCE_ITERATIONS),
])
quit(0)
func _verify_center_radii_and_isolation(planner, failures: Array[String]) -> void:
var available_tiles := _tile_catalog(28, 36, 44, 52)
var focus_position = _tile_position(32, 48, 0.5, 0.5)
var policy = POLICY_SCRIPT.new(0, 1, 0, 1, 1, 0.35)
var plan = planner.plan_runtime(available_tiles, focus_position, policy)
var wanted_tile_keys: Dictionary = plan.wanted_tile_keys()
var retained_tile_keys: Dictionary = plan.retained_tile_keys()
_expect_equal(plan.focus_tile.tile_x, 32, "center focus tile X", failures)
_expect_equal(plan.focus_tile.tile_y, 48, "center focus tile Y", failures)
_expect_equal(wanted_tile_keys.size(), 25, "center wanted 5x5", failures)
_expect_equal(retained_tile_keys.size(), 49, "center retained 7x7", failures)
_expect_true(wanted_tile_keys.has("30_46"), "wanted includes north-west radius edge", failures)
_expect_true(wanted_tile_keys.has("34_50"), "wanted includes south-east radius edge", failures)
_expect_true(retained_tile_keys.has("29_45"), "retained includes hysteresis edge", failures)
_expect_true(wanted_tile_keys.is_read_only(), "wanted result is immutable", failures)
_expect_true(retained_tile_keys.is_read_only(), "retained result is immutable", failures)
_expect_equal(available_tiles.size(), 81, "available catalog remains unchanged", failures)
func _verify_corner_prefetch_and_catalog_filter(planner, failures: Array[String]) -> void:
var available_tiles := {
"32_48": "tile-a",
"33_48": "tile-b",
"32_49": "tile-c",
"33_49": "tile-d",
"31_48": "outside-prefetch",
}
var focus_position = _tile_position(32, 48, 0.99, 0.99)
var policy = POLICY_SCRIPT.new(0, 0, 0, 0, 0, 0.35)
var plan = planner.plan_runtime(available_tiles, focus_position, policy)
_expect_set_equal(
plan.wanted_tile_keys(),
["32_48", "33_48", "32_49", "33_49"],
"corner prefetch cross product",
failures
)
_expect_set_equal(
plan.retained_tile_keys(),
["32_48", "33_48", "32_49", "33_49"],
"corner retained cross product",
failures
)
func _verify_policy_clamps(planner, failures: Array[String]) -> void:
var policy = POLICY_SCRIPT.new(16, 2, -5, -3, -4, 0.9)
_expect_equal(policy.visible_tile_radius(), 2, "visible radius keeps tile minimum", failures)
_expect_equal(policy.load_tile_radius(), 2, "negative prewarm clamps to zero", failures)
_expect_equal(policy.retained_tile_radius(), 2, "negative retain clamps to zero", failures)
_expect_near(policy.clamped_boundary_prefetch_threshold(), 0.49, "threshold upper clamp", failures)
var plan = planner.plan_runtime(
_tile_catalog(30, 34, 46, 50),
_tile_position(32, 48, 0.5, 0.5),
policy
)
_expect_equal(plan.wanted_tile_keys().size(), 25, "clamped policy wanted radius", failures)
_expect_equal(plan.retained_tile_keys().size(), 25, "clamped policy retained radius", failures)
func _verify_editor_plan(planner, failures: Array[String]) -> void:
var available_tiles := _tile_catalog(31, 33, 47, 49)
var focus_position = _tile_position(32, 48, 0.5, 0.5)
var negative_radius_plan = planner.plan_editor(available_tiles, focus_position, -4)
_expect_set_equal(
negative_radius_plan.wanted_tile_keys(),
["32_48"],
"editor negative radius clamps to focus",
failures
)
_expect_set_equal(
negative_radius_plan.retained_tile_keys(),
["32_48"],
"editor retained matches wanted",
failures
)
var radius_one_plan = planner.plan_editor(available_tiles, focus_position, 1)
_expect_equal(radius_one_plan.wanted_tile_keys().size(), 9, "editor radius one 3x3", failures)
_expect_set_equal(
radius_one_plan.retained_tile_keys(),
radius_one_plan.wanted_tile_keys().keys(),
"editor radius retained matches wanted",
failures
)
func _verify_scene_free_extraction(failures: Array[String]) -> void:
var planner_source := _read_text(PLANNER_PATH, failures)
_expect_true(planner_source.contains("extends RefCounted"), "planner is RefCounted", failures)
_expect_true(not planner_source.contains("extends Node"), "planner does not inherit Node", failures)
_expect_true(not planner_source.contains("Vector3"), "planner keeps typed position boundary", failures)
for forbidden_text in ["WorkerThreadPool", "RenderingServer", "ResourceLoader", "_tile_load_queue"]:
_expect_true(not planner_source.contains(forbidden_text), "planner omits %s" % forbidden_text, failures)
var loader_source := _read_text(LOADER_PATH, failures)
_expect_true(loader_source.contains("_streaming_target_planner.plan_runtime"), "loader delegates runtime planning", failures)
_expect_true(loader_source.contains("_streaming_target_planner.plan_editor"), "loader delegates editor planning", failures)
_expect_true(not loader_source.contains("func _predictive_focus_tiles"), "loader no longer owns prefetch planner", failures)
func _measure_planner_iterations(planner, failures: Array[String]) -> float:
var available_tiles := _tile_catalog(24, 40, 40, 56)
var focus_position = _tile_position(32, 48, 0.99, 0.99)
var policy = POLICY_SCRIPT.new(40, 5, 1, 3, 4, 0.42)
var started_microseconds := Time.get_ticks_usec()
for _iteration in PERFORMANCE_ITERATIONS:
planner.plan_runtime(available_tiles, focus_position, policy)
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
var average_plan_milliseconds := elapsed_milliseconds / float(PERFORMANCE_ITERATIONS)
_expect_true(
average_plan_milliseconds <= MAXIMUM_AVERAGE_PLAN_MILLISECONDS,
"average plan stays within %.1f ms (actual %.3f)" % [
MAXIMUM_AVERAGE_PLAN_MILLISECONDS,
average_plan_milliseconds,
],
failures
)
return elapsed_milliseconds
func _tile_catalog(minimum_tile_x: int, maximum_tile_x: int, minimum_tile_y: int, maximum_tile_y: int) -> Dictionary:
var available_tiles: Dictionary = {}
for tile_y in range(minimum_tile_y, maximum_tile_y + 1):
for tile_x in range(minimum_tile_x, maximum_tile_x + 1):
available_tiles["%d_%d" % [tile_x, tile_y]] = "synthetic"
return available_tiles
func _tile_position(tile_x: int, tile_y: int, east_fraction: float, south_fraction: float) -> GodotWorldPosition:
var tile_coordinate = ADT_TILE_COORDINATE_SCRIPT.new(tile_x, tile_y)
var tile_local_position = ADT_TILE_LOCAL_POSITION_SCRIPT.new(
east_fraction * COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS,
0.0,
south_fraction * COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
)
return COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(tile_coordinate, tile_local_position)
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_set_equal(actual_set: Dictionary, expected_keys: Array, label: String, failures: Array[String]) -> void:
var actual_keys := actual_set.keys()
actual_keys.sort()
var sorted_expected_keys := expected_keys.duplicate()
sorted_expected_keys.sort()
if actual_keys != sorted_expected_keys:
failures.append("%s expected %s, got %s" % [label, sorted_expected_keys, actual_keys])
func _expect_equal(actual_value: int, expected_value: int, label: String, failures: Array[String]) -> void:
if actual_value != expected_value:
failures.append("%s expected %d, got %d" % [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://b5unt23vi8krk
+1
View File
@@ -0,0 +1 @@
uid://caberesg37dmj
@@ -0,0 +1 @@
uid://c13xqbnau04i4
+117
View File
@@ -0,0 +1,117 @@
extends SceneTree
## Headless contract and wiring regression for the first M03 renderer facade seam.
const WORLD_RENDER_FACADE_SCRIPT := preload("res://src/render/world_render_facade.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
const RUNTIME_SCENE_PATHS: Array[String] = [
"res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
"res://src/scenes/streaming/kalimdor_streaming.tscn",
]
const TOOL_PATHS: Array[String] = [
"res://src/tools/capture_render_checkpoints.gd",
"res://src/tools/probe_render_camera_occluders.gd",
"res://src/tools/probe_render_terrain_height.gd",
]
class StreamingWorldLoaderDouble extends Node:
var current_focus: StreamingFocus
var refresh_count := 0
var metrics := {"tiles": 2, "queues": {"tile": 1}}
func set_streaming_focus(streaming_focus: StreamingFocus) -> void:
current_focus = streaming_focus
func refresh_streaming_focus(_force: bool = false) -> bool:
refresh_count += 1
return current_focus != null
func render_baseline_snapshot() -> Dictionary:
return metrics
func _initialize() -> void:
var failures: Array[String] = []
await _verify_delegation_and_snapshot_isolation(failures)
_verify_repository_wiring(failures)
if not failures.is_empty():
for failure in failures:
push_error("WORLD_RENDER_FACADE: %s" % failure)
quit(1)
return
print("WORLD_RENDER_FACADE PASS delegation=3 runtime_scenes=2 tools=3")
quit(0)
func _verify_delegation_and_snapshot_isolation(failures: Array[String]) -> void:
var test_root := Node.new()
var loader := StreamingWorldLoaderDouble.new()
loader.name = "StreamingWorldLoaderDouble"
var source := Node3D.new()
source.name = "FocusSource"
source.position = Vector3(10.0, 20.0, 30.0)
var facade = WORLD_RENDER_FACADE_SCRIPT.new()
facade.name = "WorldRenderFacade"
facade.streaming_world_loader_path = NodePath("../StreamingWorldLoaderDouble")
facade.streaming_focus_source_path = NodePath("../FocusSource")
test_root.add_child(loader)
test_root.add_child(source)
test_root.add_child(facade)
get_root().add_child(test_root)
await process_frame
_expect_true(loader.current_focus != null, "ready captures explicit source", failures)
if loader.current_focus != null:
_expect_near(loader.current_focus.world_position.x_units, 10.0, "source X", failures)
_expect_near(loader.current_focus.world_position.y_units, 20.0, "source Y", failures)
_expect_near(loader.current_focus.world_position.z_units, 30.0, "source Z", failures)
var typed_focus = STREAMING_FOCUS_SCRIPT.new(GODOT_WORLD_POSITION_SCRIPT.new(40.0, 50.0, 60.0))
facade.streaming_focus_source_path = NodePath()
facade.set_streaming_focus(typed_focus)
_expect_true(facade.refresh_streaming_focus(true), "typed focus refresh delegates", failures)
_expect_true(loader.current_focus == typed_focus, "typed focus reference delegates", failures)
var snapshot: Dictionary = facade.renderer_metrics_snapshot()
snapshot["tiles"] = 999
(snapshot["queues"] as Dictionary)["tile"] = 999
_expect_true(int(loader.metrics.tiles) == 2, "top-level metrics are detached", failures)
_expect_true(int(loader.metrics.queues.tile) == 1, "nested metrics are detached", failures)
test_root.queue_free()
func _verify_repository_wiring(failures: Array[String]) -> void:
for scene_path in RUNTIME_SCENE_PATHS:
var scene_source := _read_text(scene_path, failures)
_expect_true(scene_source.contains('path="res://src/render/world_render_facade.gd"'), "%s loads facade" % scene_path, failures)
_expect_true(scene_source.contains('[node name="WorldRenderFacade" type="Node" parent="."]'), "%s owns facade node" % scene_path, failures)
_expect_true(scene_source.contains('streaming_focus_source_path = NodePath("../ThirdPersonPlayer")'), "%s facade uses player focus" % scene_path, failures)
for tool_path in TOOL_PATHS:
var tool_source := _read_text(tool_path, failures)
_expect_true(tool_source.contains('get_node_or_null("WorldRenderFacade")'), "%s resolves facade" % tool_path, failures)
_expect_true(not tool_source.contains('world.call("refresh_streaming_focus"'), "%s avoids direct streamer refresh" % tool_path, failures)
_expect_true(not tool_source.contains('world.set("streaming_focus_source_path"'), "%s avoids direct streamer focus config" % tool_path, failures)
func _read_text(path: String, failures: Array[String]) -> String:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("cannot open %s" % path)
return ""
return file.get_as_text()
func _expect_near(actual_value: float, expected_value: float, label: String, failures: Array[String]) -> void:
if not is_equal_approx(actual_value, expected_value):
failures.append("%s expected %.3f, got %.3f" % [label, expected_value, actual_value])
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
if not actual_value:
failures.append("%s expected true" % label)
@@ -0,0 +1 @@
uid://baoiewiesd02u
+54 -16
View File
@@ -1,6 +1,7 @@
# M02 — Player Decomposition
<!-- OPENWC_TARGET:ACTIVE -->
<!-- OPENWC_TARGET:DONE -->
<!-- OPENWC_TARGET_DONE:M02:work/sindo-main-codex/m02-integrator-closeout:2026-07-15 -->
## Outcome
@@ -8,14 +9,14 @@
## Steps
- [ ] Заменить hardcoded keys на remappable Input Map actions с профилем 3.3.5a.
- [ ] Выделить `PlayerInputSource → MoveIntent`.
- [ ] Выделить local movement state/controller.
- [ ] Выделить `TerrainQuery` и убрать прямой ADT parsing из player.
- [ ] Выделить third-person camera rig и camera collision policy.
- [ ] Выделить character appearance и animation presenters.
- [ ] Оставить sprint/flight debug-функции только в sandbox profile.
- [ ] Добавить movement/camera state tests и regression scene.
- [x] Заменить hardcoded keys на remappable Input Map actions с профилем 3.3.5a.
- [x] Выделить `PlayerInputSource → MoveIntent`.
- [x] Выделить local movement state/controller.
- [x] Выделить `TerrainQuery` и убрать прямой ADT parsing из player.
- [x] Выделить third-person camera rig и camera collision policy.
- [x] Выделить character appearance и animation presenters.
- [x] Оставить sprint/flight debug-функции только в sandbox profile.
- [x] Добавить movement/camera state tests и regression scene.
## Fidelity evidence
@@ -27,10 +28,47 @@
## Evidence
- Date:
- Revision/worktree:
- Commands:
- Results:
- Fidelity comparison:
- Changed files:
- Remaining risks:
- Date: 2026-07-15
- Revision/worktree: merged master `eb122d5`; closeout
`work/sindo-main-codex/m02-integrator-closeout`.
- Commands: `verify_player_input.gd`; `verify_local_player_movement.gd`;
`verify_player_movement_capabilities.gd`; `verify_terrain_query.gd`;
`verify_third_person_camera_rig.gd`; `verify_character_presentation.gd`;
`verify_coordinate_conversion_boundaries.gd`; `verify_streaming_focus.gd`;
`tools/run_render_baseline.ps1 -DryRun`; documentation, coordination and
diff-hygiene gates.
- Results: input PASS (`16` actions, `9` intent cases, `3` profiles, one
provenance fixture, `3` real-scene cases); movement PASS (`15` cases, `2`
state transitions); profile capabilities PASS (`3` profiles, sprint/flight
allow/reject and `2` scenes); terrain PASS (`4` contract, interpolation,
cache and `2` failure cases); camera PASS (`12` state and `3` policy cases);
presentation PASS (`9` appearance and `10` animation cases across `3`
scenes); coordinate boundary PASS (`103` files, `5` consumers); StreamingFocus
PASS (`2` runtime scenes, `3` capture tools). Renderer dry-run passed project,
material, dedupe and shutdown gates, manifest `7/7/7`, calibration maximum
error `0.000015`, server-spawn renderer and all seven planned checkpoints.
- Fidelity comparison: selected build-12340 default bindings are direct private
observations pinned by `DefaultBindings.wtf` SHA-256
`35FFA0E4E2356A13A0633A848F9E57ABE7DF9D8A16C2245E15D33013AD2F56A5`
and `Bindings.xml` SHA-256
`2E01276AFB7462F1417710F13B4BF1A456341F7F28CC8AE0D6D9C7442D7F6793`;
only sanitized selected facts are committed. `3.141594` rad/s keyboard turn
is pinned to TrinityCore 3.3.5 revision
`2853a621d6af91a803787a2b8a509f4ce3e0300d`; right-mouse A/D strafe routing
is supported by WoWee revision `626243e937fb93965fa583a6507ed5a1aa7dda4b`.
These two sources are compatibility references, not direct client timing/
conditional-behavior proof. Translation speeds and camera values remain
sandbox regression baselines rather than original-client parity evidence;
jump/fall/swim/fly gameplay is not implemented by this decomposition target.
Debug sprint/free flight is proven unable to execute in `Blizzlike335`.
- Changed files: typed input intent/actions/source, local movement/capabilities,
terrain query, camera rig/collision policy, character appearance/animation
presenters, player composition, Input Map configuration, asset-free regression
scenes, sanitized input fixture, M02 verifiers, module specifications and M02
coordination claims.
- Remaining risks: direct build-12340 timing evidence for movement/camera,
jump/fall/swim/fly, autorun, left-button camera/select behavior, active camera
collision, terrain slopes/holes/liquids, persisted keybindings, server authority,
prediction and reconciliation remain for M09/M10/M12. Runtime renderer scenes
continue to default to explicit `RenderSandbox` until an application shell
selects `Blizzlike335`. M02 makes no complete `1:1` compatibility claim.
+26 -11
View File
@@ -1,6 +1,6 @@
# M03 — Renderer Facade and Safe Extraction
<!-- OPENWC_TARGET:OPEN -->
<!-- OPENWC_TARGET:ACTIVE -->
## Outcome
@@ -9,9 +9,9 @@
## Steps
- [ ] Ввести `WorldRenderFacade` для focus, environment, entity visuals, ground query и metrics.
- [ ] Запретить gameplay/editor доступ к внутренним очередям streamer.
- [ ] Извлечь pure `StreamingTargetPlanner`.
- [ ] Извлечь `RenderBudgetScheduler` с bounded queues/cancellation.
- [x] Запретить gameplay/editor доступ к внутренним очередям streamer.
- [x] Извлечь pure `StreamingTargetPlanner`.
- [x] Извлечь `RenderBudgetScheduler` с bounded queues/cancellation.
- [ ] Затем извлекать terrain, M2, WMO и liquid services по одному.
- [ ] Сохранить cache versioning и main-thread GPU finalization rules.
- [ ] Добавить dependency и performance regression checks.
@@ -26,10 +26,25 @@ Runtime и Editor используют facade; planner/scheduler тестиру
## Evidence
- Date:
- Revision/worktree:
- Commands:
- Results:
- Fidelity comparison:
- Changed files:
- Remaining risks:
- Date: 2026-07-16
- Revision/worktree: master merges `7ece2ab`, `7e35de7`; packages
`M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-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
`cases=5 average_ms=1.804`; facade/focus passed; coordinate boundary
`files=111 consumers=6`; renderer manifest retained `7/7` checkpoints;
shutdown ownership and repository gates passed. Internal-access gate additionally
derived `43` private symbols and found no forbidden access across `7` gameplay
and `9` EditorPlugin-package sources (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.
Defaults, quality presets, cache versions and visual rules are unchanged.
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;
gameplay/editor internal-access verifier; 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; M03 still needs remaining facade contracts, service extraction
and dependency/performance acceptance.
+1 -1
View File
@@ -21,7 +21,7 @@ OpenWC должен предоставить четыре согласованн
- MPQ/BLP/ADT/WDT/M2/WMO native parsing и renderer vertical slice уже существуют.
- Runtime streaming, terrain quality, MultiMesh M2, WMO caches, liquids, sky и character experiments находятся в рабочем состоянии, но сосредоточены в крупных orchestration scripts.
- Renderer baseline M00 завершён: paired comparison с оригинальным клиентом записывает измеренные gaps без заявления parity, а обязательные diagnostic gaps закрыты или явно классифицированы. M01 завершил coordinate, identity и streaming-focus seams; текущая работа M02 декомпозирует player input, movement, terrain query, camera и presentation без observable regression.
- Renderer baseline M00 завершён: paired comparison с оригинальным клиентом записывает измеренные gaps без заявления parity, а обязательные diagnostic gaps закрыты или явно классифицированы. M01 завершил coordinate, identity и streaming-focus seams; M02 декомпозировал player input, movement, terrain query, camera и presentation без observable regression. Текущая работа M03 закрывает renderer стабильным facade и извлекает planner/scheduler без rewrite.
- Gameplay domain, network protocol, production UI/Lua, audio orchestration и server adapters в основном предстоит реализовать.
- Editor plugin пока решает extraction/preview задачи, но не является полноценной authoring platform.
+3 -3
View File
@@ -10,7 +10,7 @@
## Current target
`M02` — [02-player-decomposition.md](02-player-decomposition.md)
`M03` — [03-renderer-facade.md](03-renderer-facade.md)
Одновременно `ACTIVE` может быть только одна цель. Следующая цель становится `ACTIVE` после появления валидной `OPENWC_TARGET_DONE` метки у предыдущей.
@@ -35,8 +35,8 @@
|---|---|---|---|
| M00 | [Renderer baseline](00-render-baseline.md) | — | DONE |
| M01 | [Coordinates and architecture seams](01-coordinates-and-seams.md) | M00 | DONE |
| M02 | [Player decomposition](02-player-decomposition.md) | M01 | ACTIVE |
| M03 | [Renderer facade and extraction](03-renderer-facade.md) | M02 | OPEN |
| M02 | [Player decomposition](02-player-decomposition.md) | M01 | DONE |
| M03 | [Renderer facade and extraction](03-renderer-facade.md) | M02 | ACTIVE |
| M04 | [Godot Editor shell](04-editor-shell.md) | M03 | OPEN |
| M05 | [Content Project](05-content-project.md) | M04 | OPEN |
| M06 | [Server database adapters](06-server-adapters.md) | M05 | OPEN |