fix(streaming): block login on complete world reveal

Make initial login and portal arrival consume one WorldRevealReadinessBarrier that joins near-tier mesh publication, destination composite uploads, and collision residency before normal world geometry becomes visible. This ports retail SmartBox's blocking-cell completion edge into the asynchronous client instead of exposing a ground-only login.

Add focused outdoor, indoor, texture, and invalid-claim tests; update the retail pseudocode, architecture, divergence record, issue ledger, roadmap baseline, and synchronized agent guidance.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-20 16:08:03 +02:00
parent 6c3bd4ce4b
commit b60cb67009
14 changed files with 360 additions and 102 deletions

View file

@ -98,7 +98,7 @@ production runtime failure.
**Acceptance:** A non-incremental Release solution build reports zero warnings
and zero errors without suppressing analyzers or weakening nullable checking;
the full 6,452-pass / 5-skip suite remains green.
the full 6,457-pass / 5-skip suite remains green.
---
@ -7431,6 +7431,28 @@ outdoors at the angle that previously erased it.
# Recently closed
## #229 — [DONE 2026-07-20] Initial login revealed an incomplete world
**Closed:** 2026-07-20
**Resolution:** Login auto-entry waited only for the terrain height or EnvCell
floor needed by local physics. It then removed the sky-only render gate while
the destination's static meshes, nearby scenery, and composite textures could
still be uploading. Portal travel appeared to repair the world because it
already waited on all of those domains before revealing its destination.
Login and portal arrival now share `WorldRevealReadinessBarrier`, the async
equivalent of retail `SmartBox::UseTime` keeping position completion behind
`CellManager::blocking_for_cells`. Both paths invalidate destination texture
readiness, prepare composites after mesh publication, and require the same
render-plus-collision predicate. Focused login/barrier tests and the complete
Release suite pass; capped connected visual verification remains the final
gate.
**Research:**
`docs/research/2026-07-16-portal-completion-pseudocode.md` §2.
---
## #215 — [DONE 2026-07-13] Same-dungeon death respawn removed the dungeon floor physics
**Closed:** 2026-07-13

View file

@ -20,7 +20,7 @@ banner cannot silently override the current program state.
- **Divergence audit:** 178 active rows — IA 17, AD 37, AP 85, TS 34,
and UN 5 — plus the retained retired TS-37 history note.
- **Latest automated baseline:** Release build succeeds with 17 known
test-project warnings (`#228`); 6,452 tests passed and 5 intentionally
test-project warnings (`#228`); 6,457 tests passed and 5 intentionally
skipped. The unattended connected R6 route
completed seven portal materializations, production input exercises, and a
graceful close. See

View file

@ -239,6 +239,7 @@ src/
Streaming/
StreamingController.cs -> done
GpuWorldState.cs -> done
WorldRevealReadinessBarrier.cs -> shared login/portal reveal owner
Input/
PlayerMovementController.cs -> active movement driver
Plugins/
@ -705,6 +706,19 @@ replace state produced by another job. CPU mesh extraction may still be schedule
onto `ObjectMeshManager`'s thread-safe work queue, but the worker never publishes
partially hydrated cell membership or shell placement to the renderer.
### World-reveal readiness ownership
`WorldRevealReadinessBarrier` is the single App-layer owner of the edge that
allows normal world geometry to become visible at initial login or after a
portal. It joins `StreamingController`/`GpuWorldState` static-mesh publication,
`WbDrawDispatcher` composite-texture warmup, and `PhysicsEngine` destination
residency. Login's sky-only gate and portal-space transit consume the same
predicate; they differ only in presentation. Composite preparation advances
after `WbMeshAdapter.Tick` on the render thread, never from the streaming
worker. This is acdream's asynchronous equivalent of retail
`SmartBox::UseTime` holding position completion while
`CellManager::blocking_for_cells` is set.
### Teleport cell identity at the streaming boundary
The network and physics layers carry retail's complete `Position`: an

View file

@ -67,7 +67,7 @@ accepted-divergence entries (#96, #49, #50).
|---|---|---|---|---|---|
| AD-38 | Outgoing teleport viewports retire when retail's quantized animation level exceeds the last captured visible level 1022 (index 96), suppressing levels 1023/1024 up to 20.2 ms before retail's literal `elapsed >= 1.0` state edge. Incoming fades retain the exact timer. | `src/AcDream.Core/World/TeleportAnimSequencer.cs` (`OutgoingViewportReachedTerminalProjection`) | An uncapped 2000 FPS pass can publish the finite tunnel at levels 1023/1024 even though the paired 2013 retail capture switches viewports after 1022. The table-level cutover preserves the captured visible viewport ordering without throttling the application. | Exit sound, viewport replacement, and logout tunnel entry can occur at most two easing-table quanta (about 20.2 ms) earlier than retail's logical timer. | `UIGlobals::GetAnimLevel @ 0x004EE540`; `gmSmartBoxUI::UseTime @ 0x004D6E30`; paired retail/acdream captures documented in `docs/research/2026-07-15-retail-portal-space-pseudocode.md` |
| AD-1 | Lost-cell machinery replaced by recoverable outdoor demote (**#107** safety net) + outdoor-restore `max(terrainZ, z)` under-terrain lift; retail goes `GotoLostCell` | `src/AcDream.Core/Physics/PhysicsEngine.cs:553` (+ :808) | acdream has no lost-cell state machine; outdoor landcell is the recoverable equivalent; the #107 auto-entry hold should make the demote branch unreachable | Gap in the hold → player committed to outdoor terrain inside/under a building (fake-grounded spawn, fall-through); a legit below-heightmap server restore is silently lifted — upward warp vs server | `GotoLostCell` pc:283418; `SetPositionInternal` 0x00515bd0, pc:283892-283945 |
| AD-2 | Async readiness gates replace retail's synchronous destination cell load. Login placement keeps #135's split: a hydratable indoor claim gates on its EnvCell floor (`IsSpawnCellReady`) rather than meaningless dungeon terrain; outdoor placement gates on terrain. **#218 refinement (2026-07-16):** F751 portal-space exit joins BOTH publication domains in `TeleportWorldReady`: indoor requires the center landblock's Near-tier static/EnvCell mesh set uploaded plus the EnvCell in physics, while outdoor requires that Near-tier render readiness plus terrain/collision residency for the priority near ring. Hard-recenter generations and tier-aware completion application prevent stale overlapping loads/unloads or Far/Near jobs from opening or erasing the gate; mesh upload remains separate from balanced landblock ownership. Claims beyond NumCells still take the loud forced-placement path. The hold→materialize→regain-control lifecycle remains owned by `TeleportAnimSequencer`. | `src/AcDream.App/Rendering/GameWindow.cs` (`TeleportWorldReady` + TAS transit tick); `src/AcDream.App/Streaming/StreamingController.cs` (`IsRenderNeighborhoodResident`); `src/AcDream.App/Streaming/GpuWorldState.cs` (`IsRenderReady`); `src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs`; `src/AcDream.Core/Physics/PhysicsEngine.cs` (`IsSpawnCellReady`, `IsNeighborhoodTerrainResident`) | This is the asynchronous equivalent of retail completing its blocking cell load before `EndTeleportAnimation`: neither an empty collision world, a terrain-only Far shell, nor a published-but-not-drawable GPU landblock may be revealed. Indoor does not require a terrain heightmap, only the owning render landblock and the exact EnvCell. | Gate opens early → grey/untextured reveal, free-fall, wrong-cell rooting, or missing scenery; predicate never satisfies (streamer/DAT/upload failure) → portal transit reaches its existing loud timeout/forced-placement diagnostic instead of silently hanging forever. | retail synchronous cell load before SetPosition / `gmSmartBoxUI::EndTeleportAnimation` 0x004D65A0 |
| AD-2 | Async readiness gates replace retail's synchronous destination cell load. **#229 refinement (2026-07-20):** login and F751 portal-space exit now share `WorldRevealReadinessBarrier`, so neither path can expose the normal viewport until the same render-publication, composite-texture, and collision domains converge. A hydratable indoor claim requires its owning Near-tier static/EnvCell mesh set, destination composites, and exact EnvCell physics (`IsSpawnCellReady`); an outdoor claim requires those render domains plus terrain/collision residency for the priority near ring. Hard-recenter generations and tier-aware completion application prevent stale overlapping loads/unloads or Far/Near jobs from opening or erasing the gate; mesh upload remains separate from balanced landblock ownership. Claims beyond NumCells still take the loud forced-placement path. The portal hold→materialize→regain-control lifecycle remains owned by `TeleportAnimSequencer`. | `src/AcDream.App/Streaming/WorldRevealReadinessBarrier.cs`; `src/AcDream.App/Rendering/GameWindow.cs` (login auto-entry + TAS transit tick); `src/AcDream.App/Streaming/StreamingController.cs` (`IsRenderNeighborhoodResident`); `src/AcDream.App/Streaming/GpuWorldState.cs` (`IsRenderReady`); `src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs`; `src/AcDream.Core/Physics/PhysicsEngine.cs` (`IsSpawnCellReady`, `IsNeighborhoodTerrainResident`) | This is the asynchronous equivalent of retail leaving `SmartBox::position_update_complete` false while `CellManager::blocking_for_cells` is set: neither initial login nor portal arrival may reveal an empty collision world, a terrain-only Far shell, or a published-but-not-drawable GPU landblock. Indoor does not require a terrain heightmap, only the owning render landblock and exact EnvCell. | Gate opens early → grey/untextured first login or portal reveal, free-fall, wrong-cell rooting, or missing scenery; predicate never satisfies (streamer/DAT/upload failure) → login remains behind the world render gate, while portal transit reaches its existing loud timeout/forced-placement diagnostic. | `SmartBox::UseTime` 0x00455410; `gmSmartBoxUI::EndTeleportAnimation` 0x004D65A0 |
| AD-3 | Outdoor seeds always walk the transit array (retail skips the walk when the seed CLandCell is null/unloaded); per-cell lookups no-op on unhydrated data | `src/AcDream.Core/Physics/CellTransit.cs:503` | Equivalence argument: with nothing hydrated every lookup inside the walk no-ops, so the result matches retail's skipped walk | Near partially-streamed landblocks, building-transit promotion silently can't fire until structs hydrate — membership stays outdoor while the player is inside a building | `CObjCell::find_cell_list` 0052b535-0052b56c (null-CLandCell case) |
| AD-4 | `point_in_cell` against an unhydrated CellBSP returns false (skip) rather than the null-node "inside" default; retail never queries unloaded cells | `src/AcDream.Core/Physics/CellTransit.cs:588` | The null-node default would make an unhydrated cell spuriously claim every point; skipping is the conservative streaming-safe choice | During hydration, a point genuinely inside a not-yet-loaded cell resolves outdoor/stale — transient membership misclassification driving wrong collision set and render root | `CEnvCell::find_visible_child_cell` :311397; cell-BSP vtable[0x84] |
| AD-5 | Outdoor `point_in_cell` is an identity compare against the global XY-column cell from `LandDefs.AdjustToOutside` (no per-cell containment test) | `src/AcDream.Core/Physics/CellTransit.cs:865` | Landcells are disjoint 24 m columns — identity-compare against the column under the sphere centre is exactly equivalent to retail's per-candidate test | If block-origin/lcoord math is wrong at a landblock seam, the compare silently never matches — outdoor membership freezes at boundaries (the pre-#106 symptom) | `find_cell_list` pick pc:308788-308825; `CLandCell::point_in_cell` (get_block_offset pc:308804) |

View file

@ -920,7 +920,7 @@ diagnostic scaffolding, not yet the final collision system.
PositionManager facade remains the final R6 ownership cleanup.
Final 2026-07-20 gate: three independent retail/architecture/adversarial
re-reviews are clean; Release builds successfully with 17 known test-project
warnings tracked by #228; the repository suite is 6,452 passed / 5 skipped.
warnings tracked by #228; the repository suite is 6,457 passed / 5 skipped.
The unattended connected Release rebaseline also
passes: seven portal materializations across Caul, Sawato, Rynthid,
Aerlinthe, Holtburg, and same-location revisits; production-dispatcher turn,
@ -1446,7 +1446,7 @@ OpenGL ceiling; revisit macOS only if a supported graphics backend is chosen.
## Cross-cutting work tracked in parallel
- **Test coverage.** Each phase lands with layer-matched unit and integration tests in `tests/`. Current Release baseline: 6,452 passed / 5 intentional skips (2026-07-20).
- **Test coverage.** Each phase lands with layer-matched unit and integration tests in `tests/`. Current Release baseline: 6,457 passed / 5 intentional skips (2026-07-20).
- **Memory files.** Live subsystem state and digests are indexed by `claude-memory/MEMORY.md`; stable engineering references remain under `memory/`. Canonical program status stays in milestones/roadmap/issues.
- **`CLAUDE.md` discipline.** Check all four references (ACE, ACViewer, WorldBuilder, Chorizite) before committing to an approach. WorldBuilder is the closest stack match and should be checked first.

View file

@ -19,7 +19,7 @@ Carried:
registered TS-50/TS-51 timing residuals (the complete-root-Frame/object-workset
cutover is automated-test complete 2026-07-20; three independent re-reviews are
clean, Release builds with 17 known test-project warnings tracked by #228, and
6,452 tests pass / 5 skip), and
6,457 tests pass / 5 skip), and
the deferred Modern Pipeline track (MP1b+). The separate #225 shared-alpha
implementation has passed connected lifetime/performance gates and awaits only
its lifestone/particle visual comparison.

View file

@ -64,7 +64,7 @@ sequence without advancing time and republish all part transforms. Forcing a
Ready motion, recognizing recall by id, or freezing the previously published
part pose would all diverge from this general retail mechanism.
## 2. Portal exit waits for render publication as well as collision data
## 2. Login and portal exit wait for render publication as well as collision data
Named retail references:
@ -82,6 +82,15 @@ that teleport is no longer in progress,
`EndTeleportAnimation` begins `TAS_TUNNEL_CONTINUE`; the normal world is not
revealed before the blocking load edge has completed.
The same `SmartBox::UseTime` edge governs initial position completion. It does
not distinguish “enough collision to stand” from “enough render state to show
the world”: while `CellManager::blocking_for_cells` is set it only checks
prefetch status, and only after the block clears does it change the player's
position, set `position_update_complete`, advance object/physics/landscape
time, and draw the normal scene. Therefore acdream's login auto-entry cannot
gate on one sampled terrain height while portal arrival uses a complete render
barrier. Both presentations must consume one shared asynchronous equivalent.
acdream's load is split into two independent readiness domains:
```text
@ -98,18 +107,28 @@ worker/register path
Required asynchronous equivalent:
```text
TeleportWorldReady(destination):
WorldRevealReady(destination):
if destination claim cannot be hydrated:
return true so the existing loud forced-placement path diagnoses it
if destination is indoor:
return render center landblock Near-tier and ready
AND destination composite textures ready
AND destination EnvCell collision data ready
return every landblock in the priority near ring Near-tier and render-ready
AND destination composite textures ready
AND every landblock in that ring collision-resident
```
`WorldRevealReadinessBarrier.Begin` invalidates destination-scoped composite
readiness for both the first accepted player position and each accepted portal
destination. After the render-thread mesh tick, `Prepare` advances composite
uploads only when the required static meshes are published. Login remains
behind its sky-only render gate and portal travel remains in its tunnel until
the same `IsReady` predicate opens. This prevents the two paths from drifting
apart again while preserving their different presentations.
Streaming completion is generation- and tier-aware. Every hard recenter,
dungeon collapse, and dungeon expansion advances a token carried by Load,
Promote, Unload, and their results. This rejects an old build even when its id