fix(portal): synchronize destination presentation state
This commit is contained in:
parent
4b1bceefbb
commit
e95f55f25b
42 changed files with 2815 additions and 288 deletions
|
|
@ -44,6 +44,76 @@ Copy this block when adding a new issue:
|
|||
|
||||
---
|
||||
|
||||
## #218 — Portal silhouette pose, destination reveal, and indoor observer snap
|
||||
|
||||
**Status:** IN-PROGRESS — root fixes implemented 2026-07-16; pending connected visual gate
|
||||
**Severity:** HIGH
|
||||
**Filed:** 2026-07-16
|
||||
**Component:** portal VFX / streaming / physics / outbound movement
|
||||
|
||||
**Description:** The purple player silhouette in portal space sampled the tail
|
||||
of the recall action instead of the finished Ready pose. Some outdoor exits
|
||||
briefly revealed grey/unloaded landblocks during the view-plane zoom. A retail
|
||||
observer watching acdream enter a dungeon saw movement while commands were
|
||||
active, but stopping snapped acdream back to the dungeon entrance.
|
||||
|
||||
**Root cause / status:** These were three lifecycle truths hidden by the same
|
||||
portal gate. Hidden entities skipped part-pose composition entirely, so
|
||||
`HandleEnterWorld` changed the sequence cursor to Ready without publishing that
|
||||
pose before the zero-time Hidden PES emitted the silhouette. Portal readiness
|
||||
checked collision residency but not actual mesh/texture render readiness; the
|
||||
captured destination additionally failed whole landblock builds when dense
|
||||
procedural scenery exceeded the obsolete 256-id namespace. Finally,
|
||||
`PhysicsBody.CellPosition` advanced only outdoors: indoor resolves updated the
|
||||
controller cell but left the canonical outbound frame at the portal entrance.
|
||||
|
||||
The implementation now samples Hidden sequence poses without advancing time,
|
||||
joins render and collision readiness before world reveal, uses a collision-free
|
||||
`0x8XXYYIII` 4,096-entry scenery namespace, and commits every successful indoor
|
||||
transition's full cell/frame before movement serialization. Render readiness
|
||||
tracks both static GfxObjs and EnvCell shell geometry through completed WB
|
||||
uploads and requires the complete priority ring to be Near-tier. Stale worker
|
||||
completions carry a hard-recenter generation, so even overlapping old loads and
|
||||
unloads cannot overwrite the replacement window. A Near load demoted before
|
||||
its first publication is converted to the equivalent terrain-only Far payload
|
||||
instead of leaving a hole. Mesh upload no longer manufactures a second logical
|
||||
owner; static and synthetic EnvCell geometry pins release symmetrically, while
|
||||
zero-owner late uploads stay evictable. EnvCell publication replays its
|
||||
schema-aware preparation request after those pins are installed, closing an
|
||||
eviction race without routing synthetic ids through generic GfxObj decode. The
|
||||
bounded CPU cache retains texture payloads and re-stages evicted meshes exactly
|
||||
once instead of returning a never-uploaded or blank cache hit. A self-contained
|
||||
promotion that supersedes a queued Far load now publishes directly as a real
|
||||
Near landblock, so readiness cannot wait forever for a base the streamer
|
||||
intentionally cancelled; later Far completion cannot overwrite that Near tier.
|
||||
Near-to-Far now tears down the full Near-only App/Core layer while preserving
|
||||
terrain, and repeated current-generation Near completions are ignored before
|
||||
they can duplicate statics, scripts, pins, or callbacks. Static collision is
|
||||
removed by owner before prefix cleanup, including footprints flooded across a
|
||||
landblock seam, while server-live/dynamic registrations remain refloodable.
|
||||
Full unload uses the same owner rule; surviving owners seeded across the seam
|
||||
track withdrawn prefixes so a later reload restores their missing collision
|
||||
rows.
|
||||
|
||||
**Files:** `src/AcDream.Core/Physics/AnimationSequencer.cs`;
|
||||
`src/AcDream.App/Rendering/GameWindow.cs`;
|
||||
`src/AcDream.App/Streaming/StreamingController.cs`;
|
||||
`src/AcDream.App/Streaming/GpuWorldState.cs`;
|
||||
`src/AcDream.App/Rendering/Wb/LandblockSpawnAdapter.cs`;
|
||||
`src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs`;
|
||||
`src/AcDream.Core/World/ProceduralSceneryIdAllocator.cs`;
|
||||
`src/AcDream.Core/Physics/PhysicsBody.cs`;
|
||||
`src/AcDream.App/Input/PlayerMovementController.cs`.
|
||||
|
||||
**Research:** `docs/research/2026-07-16-portal-completion-pseudocode.md`.
|
||||
|
||||
**Acceptance:** Recall/portal Hidden particles outline the finished standing
|
||||
pose; outdoor exits reveal only fully rendered terrain/scenery; and a retail
|
||||
observer sees acdream run and stop repeatedly inside a dungeon without any
|
||||
snap back to the entrance.
|
||||
|
||||
---
|
||||
|
||||
## #217 — Character windows did not receive live 64-bit experience updates
|
||||
|
||||
**Status:** DONE — 2026-07-13, connected gate user-confirmed
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| # | Divergence | Where (file:line) | Why it is safe / justified | Risk if assumption breaks | Retail oracle |
|
||||
|---|---|---|---|---|---|
|
||||
| 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 spawn gates replacing retail's synchronous cell load. **#135 refinement:** an INDOOR spawn/teleport (cell ≥ 0x0100, hydratable) gates ONLY on the EnvCell floor (`IsSpawnCellReady`), NOT the terrain heightmap; an OUTDOOR spawn (or an unhydratable indoor claim that demotes outdoor) gates on the terrain-ready hold (**#106**). A dungeon's negative-offset cells can place the spawn's WORLD position in a neighbour terrain landblock the #135 dungeon collapse doesn't load, so a terrain requirement would hang indoor login/teleport forever (cellReady true, terrain null) — the player lands on the cell floor, terrain is irrelevant indoors. Claims beyond NumCells skip the gate (demoted). **#145 refinement (2026-06-22):** an OUTDOOR teleport additionally requires the destination's OWN landblock terrain to be resident (`PhysicsEngine.IsLandblockTerrainResident(destCell)`, which `StreamingController` priority-applies so it flips in ~hundreds of ms) before placing — and `SampleTerrainZ` is short-circuited behind it. On a teleport OUT of a dungeon the collapsed SOURCE dungeon stays resident at the same streaming-local coords and answers `SampleTerrainZ` (terrain 0), so without the load check the resolve ran against the dungeon's cells and rooted the player at a dungeon-frame cell id (ACE then rejected all movement as "failed transition"). Decision in `TeleportWorldReady`; the hold→materialize→regain-control transit is driven by `TeleportAnimSequencer` (the retail portal/view-plane lifecycle, ticked in `OnUpdate`), which replaced the retired `TeleportArrivalController` | `src/AcDream.App/Rendering/GameWindow.cs` (`isSpawnGroundReady` lambda ~1010 + `TeleportWorldReady` ~5512 + the TAS transit tick in `OnUpdate`) (+ `src/AcDream.App/Input/PlayerModeAutoEntry.cs:69`, `src/AcDream.Core/Physics/PhysicsEngine.cs:468`) | Entering earlier integrates gravity against an empty world (free-fall into void); the gate is the async-streaming equivalent of retail's blocking load; a looser "any struct present" version reproduced the transparent-interior wedge. Indoor-on-cellReady is the faithful equivalent of retail's synchronous cell load + place-on-floor (terrain under a dungeon is meaningless; the pre-#135 terrain hold only passed because the 25×25 window streamed the neighbour terrain) | Gate opens early → raw claim commit → outdoor demote mid-building; predicate never satisfied (streamer stall, dat edge case) → login wedges in pre-player mode; an indoor spawn whose cell never hydrates now holds on cellReady alone (no terrain backstop) — but that path is exactly the #107 hold | retail synchronous cell load before SetPosition (no gate exists) |
|
||||
| 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-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) |
|
||||
|
|
|
|||
|
|
@ -125,6 +125,27 @@ before installing retail's distant light. The lifecycle, camera, animation,
|
|||
and draw ordering are acdream-owned ports of `gmSmartBoxUI`; WorldBuilder never
|
||||
implemented this UI viewport.
|
||||
|
||||
**Portal destination render-readiness seam (2026-07-16).**
|
||||
`GpuWorldState.IsRenderReady` does not treat dictionary publication as a draw
|
||||
barrier. `LandblockSpawnAdapter` retains the complete required-id set for each
|
||||
landblock: atlas-tier GfxObjs plus the synthetic geometry ids prepared by the
|
||||
independent EnvCell shell pipeline. `WbMeshAdapter.IsRenderDataReady` opens the
|
||||
gate only after `ObjectMeshManager` has real GPU render data. Its bounded CPU
|
||||
cache retains texture payloads and stages a missing GPU object through a
|
||||
deduplicated upload queue on cache hit, covering eviction and revisit churn.
|
||||
GPU upload is deliberately not an ownership acquire: atlas GfxObjs use their
|
||||
landblock/entity pins, while synthetic EnvCell geometry uses a no-generic-decode
|
||||
pin balanced from each landblock snapshot. A late upload after all owners have
|
||||
released enters the evictable LRU instead of resurrecting a reference. After
|
||||
publication pins the synthetic ids, the controller replays their immutable
|
||||
environment/cell-structure/surface preparation descriptors; if a formerly
|
||||
unowned mesh was evicted between worker scheduling and publication, this
|
||||
schema-aware replay re-stages it without a generic GfxObj lookup.
|
||||
Near-to-Far demotion is a separate App transaction: it releases EnvCell
|
||||
rendering and landblock mesh pins while retaining the terrain slot. Core's
|
||||
matching physics demotion preserves the terrain surface but removes indoor
|
||||
cells, portals, buildings, and static shadow registrations.
|
||||
|
||||
**Workflow:** Before re-implementing any AC-specific rendering or dat-handling
|
||||
algorithm, **check this inventory first**. If we already extracted it (🟢
|
||||
sections), it's in `src/AcDream.App/Rendering/Wb/` — use our copy. If WB has
|
||||
|
|
@ -194,6 +215,13 @@ Retail `TexMerge::CopyAndTile` (`0x00503580`) and `TexMerge::Merge`
|
|||
| `SceneryHelpers` | Displace / RotateObj / ScaleObj / ObjAlign / CheckSlope |
|
||||
| `SceneryInstance` | Per-spawn instance data |
|
||||
|
||||
acdream's streamed projection assigns generated instances local runtime IDs
|
||||
through `AcDream.Core.World.ProceduralSceneryIdAllocator`. The namespace is
|
||||
`0x8XXYYIII` (full X/Y bytes plus a 12-bit counter); bit 31 remains the stable
|
||||
scenery classification seam. This is projection identity, not placement
|
||||
behavior. The former 8-bit counter rejected dense retail-DAT landblocks before
|
||||
their render transaction could publish (#218).
|
||||
|
||||
### Static objects (buildings, slabs, props — Setup + GfxObj + ObjDesc)
|
||||
|
||||
| Component | What it does |
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ active enchantment, then recall through the DAT-driven portal presentation.
|
|||
3. **Pending user visual gate:** connected projectile/self-buff casts plus the
|
||||
complete retained magic UI interaction checklist.
|
||||
4. **Pending user visual gate:** two-client portal-out/materialization observer
|
||||
presentation before M3 closes.
|
||||
presentation before M3 closes, including #218's finished Hidden silhouette,
|
||||
fully rendered destination reveal, and no indoor stop-time position snap.
|
||||
|
||||
Opportunistic UI/physics fixes are no longer the work-order driver. They enter
|
||||
the active slice only when they block this demo or represent a severe regression.
|
||||
|
|
|
|||
|
|
@ -332,6 +332,37 @@ the Lifestone recall boundary and a MagicPortal casting timeline. The user
|
|||
confirmed `/ls` and spell-recall arrivals no longer replay their animation
|
||||
tails; additional portal visual polish is explicitly deferred.
|
||||
|
||||
**2026-07-16 corrective portal gate (#218).** A side-by-side retail observer
|
||||
check found three distinct state-publication defects after the arrival-tail
|
||||
fix. Hidden now recomposes the post-`HandleEnterWorld` cyclic pose without
|
||||
advancing animation time before its zero-time purple PES hooks run. Portal
|
||||
space now waits for both completed static/EnvCell mesh uploads (including the
|
||||
texture flush) and collision residency; dense
|
||||
outdoor landblocks no longer abort at the obsolete 256-entry procedural
|
||||
scenery id boundary (`0x8XXYYIII`, 4,096 entries). Finally, successful indoor
|
||||
transitions commit the full resolved EnvCell/frame into the canonical outbound
|
||||
Position, preventing stop-time snaps to the dungeon entrance on retail
|
||||
observers. GPU-evicted meshes are re-staged from a bounded texture-complete CPU
|
||||
cache instead of becoming false-ready or permanently absent. Portal readiness
|
||||
also requires Near-tier ownership throughout the ring; stale worker completions
|
||||
carry a hard-recenter generation and cannot overwrite a newer tier/window, and
|
||||
a first Near completion demoted in flight is published as its equivalent
|
||||
terrain-only Far payload rather than dropped. Upload completion is separate
|
||||
from logical mesh ownership; landblock/static/EnvCell pins now balance across
|
||||
unload and zero-owner late uploads remain evictable. A self-contained promotion
|
||||
that supersedes a queued Far load publishes directly as a real Near landblock;
|
||||
there is no provisional transaction waiting indefinitely for a base that the
|
||||
streamer intentionally cancelled. Near-to-Far now has an explicit cross-layer
|
||||
demotion transaction that preserves terrain while retiring EnvCells, indoor
|
||||
physics/buildings, static collision, lights, translucency state, entities, and
|
||||
render pins. Cross-landblock static shadow footprints are removed by logical
|
||||
owner while dynamic registrations remain refloodable; duplicate Near
|
||||
completions are publication-idempotent. Full unload shares the static-owner
|
||||
teardown, and surviving adjacent owners remember withdrawn prefixes so their
|
||||
cross-seam rows return when a landblock is promoted again.
|
||||
Automated gates are green; the connected single-/two-client visual gate is
|
||||
pending.
|
||||
|
||||
---
|
||||
|
||||
#### (historical M1.5 working notes below)
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ OUTSIDE SLICE — DrawLandscapeThroughOutsideView (RetailPViewRenderer.cs:214-23
|
|||
|
||||
SKYRENDERER — src/AcDream.App/Rendering/Sky/SkyRenderer.cs (WB SkyboxRenderManager port): RenderSky draws the pre-scene partition (Properties bit 0x01 clear), RenderWeather the post-scene partition (bit set) (:106-144, 219-235 — correct retail bit semantics, citing MakeObject 0x00506ee0). GL state: depth test OFF + depth mask OFF + cull off + per-submesh additive/alpha blend (:194-210) — equivalent to retail's DEPTHTEST_ALWAYS/no-write. View translation zeroed = camera-centred sky (:175-178). Weather −120 offset applied as a CAMERA-RELATIVE model translation for bit4 && !bit8 objects (:307-308). Fog-override bit 0x02 skip implemented (:240-241). sky.vert writes gl_ClipDistance from the terrain-clip UBO (src/AcDream.App/Rendering/Shaders/sky.vert:153) so the doorway slices genuinely clip sky and rain.
|
||||
|
||||
SCENERY — SceneryGenerator.Generate (src/AcDream.Core/World/SceneryGenerator.cs:86 via WbSceneryAdapter) → GameWindow.BuildSceneryEntitiesForStreaming (GameWindow.cs:5290-5473): scenery becomes WorldEntity ids 0x80XXYYII with MeshRefs and NO ParentCellId (5463-5472) → lands in the Outdoor partition bucket → drawn once per outside slice with per-entity frustum cull + slice clip planes. Building suppression at generation uses a 9×9 vertex-grid set derived from building origins (5310-5316). There are no per-LandCell object lists outdoors — the bucket is flat.
|
||||
SCENERY — SceneryGenerator.Generate (src/AcDream.Core/World/SceneryGenerator.cs:86 via WbSceneryAdapter) → GameWindow.BuildSceneryEntitiesForStreaming (GameWindow.cs:5290-5473): scenery becomes WorldEntity ids 0x8XXYYIII (bit-31 class + X8/Y8/counter12) with MeshRefs and NO ParentCellId (5463-5472) → lands in the Outdoor partition bucket → drawn once per outside slice with per-entity frustum cull + slice clip planes. Building suppression at generation uses a 9×9 vertex-grid set derived from building origins (5310-5316). There are no per-LandCell object lists outdoors — the bucket is flat.
|
||||
|
||||
## DIVERGENCES
|
||||
|
||||
|
|
|
|||
228
docs/research/2026-07-16-portal-completion-pseudocode.md
Normal file
228
docs/research/2026-07-16-portal-completion-pseudocode.md
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
# Portal completion: hidden pose, destination residency, and indoor position
|
||||
|
||||
This note records the retail mechanisms behind three connected portal defects
|
||||
observed on 2026-07-16. The named Sept-2013 retail client is the behavioral
|
||||
oracle. The render-residency portion is an acdream adaptation because retail
|
||||
loads cells synchronously while acdream publishes streamed landblocks
|
||||
asynchronously.
|
||||
|
||||
## 1. Hidden transition samples the post-link cyclic pose
|
||||
|
||||
Named retail references:
|
||||
|
||||
- `CPhysicsObj::set_hidden` at `0x00514C60`
|
||||
- `CPartArray::HandleEnterWorld` at `0x00517D70`
|
||||
- `MotionTableManager::HandleEnterWorld` at `0x0051BDD0`
|
||||
- `CSequence::remove_all_link_animations` at `0x00524CA0`
|
||||
- `CPhysicsObj::UpdateObjectInternal` at `0x005156B0`
|
||||
- `CPhysicsObj::UpdatePositionInternal` at `0x00512C30`
|
||||
- `CPhysicsObj::set_frame` at `0x00514090`
|
||||
- `CPartArray::SetFrame` at `0x00519310`
|
||||
- `CPartArray::UpdateParts` at `0x005190F0`
|
||||
|
||||
Installed retail DAT confirmation:
|
||||
|
||||
- Lifestone Recall `0x10000153` resolves to animation `0x030009BF`.
|
||||
- Its ACE-duration boundary is `(150 - 0) / 9.96 = 15.06024096 s`.
|
||||
- At that exact boundary the non-looping recall node remains at its final
|
||||
authored frame (approximately frame 150); it has not advanced the cyclic
|
||||
Ready node yet.
|
||||
- Hidden PES `0x33000331` contains fourteen `CreateParticle` hooks and one
|
||||
translucency hook, all at time zero. The pose published when Hidden starts
|
||||
therefore determines the purple player silhouette.
|
||||
|
||||
Retail pseudocode:
|
||||
|
||||
```text
|
||||
SetState(visible -> Hidden):
|
||||
set_hidden(true)
|
||||
play typed Hidden PES
|
||||
part_array.HandleEnterWorld()
|
||||
|
||||
MotionTableManager.HandleEnterWorld(sequence):
|
||||
sequence.remove_all_link_animations()
|
||||
|
||||
sequence.remove_all_link_animations():
|
||||
discard link/action nodes
|
||||
if the current node was the final link before the cyclic tail:
|
||||
current = first_cyclic
|
||||
frame = first_cyclic.starting_frame
|
||||
|
||||
next hidden CPhysicsObj.UpdateObjectInternal:
|
||||
do not advance PartArray animation time
|
||||
UpdatePositionInternal(...)
|
||||
later set_frame(current physics frame)
|
||||
PartArray.SetFrame(frame)
|
||||
PartArray.UpdateParts(frame)
|
||||
sample sequence.get_curr_animframe()
|
||||
ScriptManager executes the zero-time Hidden PES hooks
|
||||
```
|
||||
|
||||
Port rule: Hidden suppresses time advance, not pose composition. After
|
||||
`HandleEnterWorld` changes the sequence cursor, acdream must sample the current
|
||||
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
|
||||
|
||||
Named retail references:
|
||||
|
||||
- `gmSmartBoxUI::BeginTeleportAnimation` at `0x004D6300`
|
||||
- `gmSmartBoxUI::EndTeleportAnimation` at `0x004D65A0`
|
||||
- `gmSmartBoxUI::UseTime` at `0x004D6E30`
|
||||
- `SmartBox::teleport_in_progress` at `0x00451C20`
|
||||
- `SmartBox::UseTime` at `0x00455410`
|
||||
|
||||
Retail performs the destination load behind the portal-space viewport.
|
||||
`SmartBox::teleport_in_progress` remains true while the player exists and
|
||||
`position_update_complete == 0`; `SmartBox::UseTime` only advances that
|
||||
position completion when `blocking_for_cells == 0`. When the SmartBox reports
|
||||
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.
|
||||
|
||||
acdream's load is split into two independent readiness domains:
|
||||
|
||||
```text
|
||||
worker parses/builds landblock
|
||||
-> render thread uploads terrain/entities
|
||||
-> GpuWorldState.AddLandblock(canonical id)
|
||||
-> WbMeshAdapter.Tick uploads every required static/EnvCell mesh
|
||||
and flushes its staged texture-array updates // render readiness
|
||||
|
||||
worker/register path
|
||||
-> PhysicsEngine registers terrain or EnvCell // collision publication
|
||||
```
|
||||
|
||||
Required asynchronous equivalent:
|
||||
|
||||
```text
|
||||
TeleportWorldReady(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 EnvCell collision data ready
|
||||
|
||||
return every landblock in the priority near ring Near-tier and render-ready
|
||||
AND every landblock in that ring collision-resident
|
||||
```
|
||||
|
||||
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
|
||||
overlaps the replacement region, and rejects a completed old unload that would
|
||||
otherwise tear down new state. Within one generation, a stale Far completion
|
||||
cannot overwrite a published or pending Near landblock. If an initial Near load
|
||||
finishes only after the region has demoted that still-unpublished landblock to
|
||||
Far, its already-built heightmap and mesh are published through a new
|
||||
terrain-only `LandblockBuild` (empty entity list, no EnvCell transaction, empty
|
||||
physics bundle). This is byte-for-behavior the payload a fresh `LoadFar` would
|
||||
build, without a second DAT read or a missing terrain slot.
|
||||
|
||||
GPU upload existence is not logical ownership. Landblock registrations pin
|
||||
static GfxObjs; EnvCell transactions use a distinct no-decode pin for their
|
||||
synthetic shell ids. Upload completion preserves that owner count rather than
|
||||
incrementing it. Unload releases the matching per-landblock snapshots, and a
|
||||
late upload whose owners already released enters the evictable LRU immediately.
|
||||
Because an unowned EnvCell mesh can be evicted between the worker's first
|
||||
schedule and landblock publication, publication pins every synthetic id first
|
||||
and then replays the immutable environment/cell-structure/surface request.
|
||||
The second schema-aware call is a no-op while data remains resident and a
|
||||
correct CPU-cache restage or specialized decode when it does not.
|
||||
`PromoteToNear` carries a complete build and terrain mesh because the streamer
|
||||
allows it to supersede a queued Far load. It therefore publishes directly as a
|
||||
real Near landblock when no base exists, installing EnvCells, collision, lights,
|
||||
registries, entities, and render pins in one transaction. If a Far load had
|
||||
already started, its later completion cannot overwrite the published Near tier.
|
||||
Normal demotion and unload then own the same teardown path as any other Near
|
||||
landblock. Demotion explicitly retires EnvCell visibility/rendering, indoor
|
||||
cell and building physics, static shadow objects, lights, translucency owners,
|
||||
static entities, and mesh pins while preserving the terrain render slot and
|
||||
terrain physics surface. Static shadow owners are deregistered by their seed
|
||||
landblock before cell-prefix removal, so footprints flooded across a landblock
|
||||
seam cannot survive as invisible collision; dynamic owners remain refloodable.
|
||||
Full unload uses the same owner teardown. For owners seeded in an adjacent
|
||||
resident block, the registry remembers which streamed-out prefix withdrew
|
||||
their cross-seam rows and includes them when that prefix refloods, restoring
|
||||
collision on promotion without resurrecting a removed owner.
|
||||
Duplicate same-generation Near completions are ignored
|
||||
at the publication owner, so they cannot append statics or replay scripts.
|
||||
There is no provisional side state waiting for a base that may never arrive.
|
||||
|
||||
`GpuWorldState.IsRenderReady` joins state publication with
|
||||
`LandblockSpawnAdapter`'s required-mesh set. That set contains atlas-tier
|
||||
GfxObjs and the synthetic geometry ids used by EnvCell shells, and it remains
|
||||
false until `WbMeshAdapter.TryGetRenderData` succeeds for every id after a
|
||||
render-thread tick and texture flush. A timer or black fade is not a substitute.
|
||||
|
||||
GPU eviction retains a bounded re-uploadable CPU mesh payload. A cache hit
|
||||
re-stages that payload exactly once, preserving texture bytes, so revisit and
|
||||
portal churn cannot strand the readiness gate behind a CPU-only cache entry.
|
||||
Failed uploads retain queue ownership only through their bounded loud retry
|
||||
sequence; they cannot create duplicate uploads or a false-ready landblock.
|
||||
|
||||
The 2026-07-16 trace also found that dense outdoor landblocks failed before
|
||||
publication: the procedural scenery id allocator reserved only 256 ids per
|
||||
landblock although the retail DAT generator can produce more. The stable local
|
||||
id namespace therefore uses the same collision-free field allocation already
|
||||
used by interior statics:
|
||||
|
||||
```text
|
||||
procedural scenery id = 0x8XXYYIII
|
||||
class prefix: 4 bits (0x8)
|
||||
landblock X: 8 bits
|
||||
landblock Y: 8 bits
|
||||
per-landblock counter: 12 bits (0..4095)
|
||||
```
|
||||
|
||||
All scenery consumers classify on bit 31 and no consumer decodes the old byte
|
||||
layout. Overflow must fail before aliasing the next landblock.
|
||||
|
||||
WorldBuilder cross-check: its upload queue similarly distinguishes generated
|
||||
CPU scenery from render-thread GPU publication (`GameScene.ProcessUploads`),
|
||||
which confirms that worker completion alone is not draw readiness.
|
||||
|
||||
## 3. Successful indoor transitions commit the canonical outbound Position
|
||||
|
||||
Named retail references:
|
||||
|
||||
- `CPhysicsObj::SetPositionInternal(CTransition const*)` at `0x00515330`
|
||||
- `CPhysicsObj::UpdateObjectInternal` at `0x005156B0`
|
||||
- `CommandInterpreter::SendMovementEvent` at `0x006B4680`
|
||||
- `CommandInterpreter::SendPositionEvent` at `0x006B4770`
|
||||
- `CommandInterpreter::ShouldSendPositionEvent` at `0x006B45E0`
|
||||
|
||||
Retail pseudocode:
|
||||
|
||||
```text
|
||||
after a successful transition:
|
||||
if transition.curr_cell == current cell:
|
||||
m_position.objcell_id = transition.curr_pos.objcell_id
|
||||
else:
|
||||
change_cell(transition.curr_cell)
|
||||
set_frame(transition.curr_pos.frame)
|
||||
|
||||
SendMovementEvent:
|
||||
serialize MoveToStatePack(..., player.m_position, ...)
|
||||
|
||||
SendPositionEvent:
|
||||
serialize AutonomousPositionPack(player.m_position, ...)
|
||||
```
|
||||
|
||||
This applies equally to outdoor position cells and indoor EnvCells. The local
|
||||
frame origin advances by the resolved world displacement, and a successful
|
||||
transition adopts the resolver's exact destination cell id. Outdoor positions
|
||||
then use `LandDefs.AdjustToOutside` to canonicalize landblock/cell boundaries;
|
||||
indoor positions retain their EnvCell-relative frame and adopt the resolved
|
||||
EnvCell id.
|
||||
|
||||
The previous acdream behavior changed only the controller's visible `CellId`
|
||||
for indoor motion. `PhysicsBody.CellPosition`, which is what outbound movement
|
||||
serializes, stayed at the dungeon portal-entry frame. ACE could predict motion
|
||||
while commands continued, but the next authoritative idle position returned
|
||||
the retail observer to that stale frame. The correct port commits the resolved
|
||||
cell and frame into the one canonical `PhysicsBody.CellPosition`; it does not
|
||||
add resends, grace periods, or observer-specific correction.
|
||||
Loading…
Add table
Add a link
Reference in a new issue