feat(vfx): port retail hidden and teleport presentation

Preserve canonical live-object ownership across Hidden transitions and remote teleport placement so effects, collision, streaming, and targeting remain synchronized.
This commit is contained in:
Erik 2026-07-14 14:59:48 +02:00
parent a51ebc66e9
commit 1e98d81448
46 changed files with 4883 additions and 127 deletions

View file

@ -181,6 +181,14 @@ src/
AcDream.App/ Layer 1 + Layer 4 wiring
Physics/
ProjectileController.cs -> live-record projectile orchestration/corrections
RemotePhysicsUpdater.cs -> ordinary/Hidden remote narrow-tick integration
RemoteTeleportController.cs -> incarnation-scoped loaded/pending placement owner
RemoteTeleportHook.cs -> ordered retail teleport teardown seam
RemoteTeleportPlacement.cs -> collision-seated SetPosition transition commit
World/
LiveEntityRuntime.cs -> canonical logical identity/state/spatial ownership
LiveEntityPresentationController.cs -> Hidden/NoDraw/effect/collision presentation
LiveEntityTeardown.cs -> failure-isolated multi-owner lifecycle drain
Rendering/
GameWindow.cs -> still owns too much runtime wiring
TerrainRenderer.cs -> done
@ -248,6 +256,36 @@ What exists and is active:
owns the frame;
delete, generation replacement, pickup/parent leave-world, and session reset
use `LiveEntityRuntime`'s normal lifecycle and never create a second GUID map.
- `RemoteTeleportController` owns the placement half of a fresh remote
teleport after `RemoteTeleportHook` has torn down movement/target state. It
collision-seats loaded destinations through `RemoteTeleportPlacement`; an
unloaded destination retains one generation- and PositionSequence-scoped
pending placement and resolves the latest accepted frame when that same
projection becomes visible. It neither owns GUID identity nor reconstructs
an entity. A placement failure after hydration restores the captured source
frame/cell/contact rather than leaving a visible collisionless projection;
source-resident shadows restore immediately, while an unloaded source
delegates one incarnation-scoped restore to
`LiveEntityPresentationController`, shared with Hidden/UnHide. A newer
placement transfers that marker into an explicit active-placement generation
before rebucketing even while Hidden. That generation suppresses every
intervening Hidden/UnHide and projection restore until the controller reaches
a stable result, then it either restores after collision seating, re-defers
its rollback source, or hands a Hidden result back for UnHide. The typed
`ILiveEntityRemotePlacementRuntime` seam permits a same-incarnation wrapper
rebind only around the canonical body; pending placement adopts that wrapper
before hydration and cannot silently lose ownership. Clearing a motion or
projectile component retains the incarnation's body/contract identity until
logical teardown. The production wrapper exposes an immutable body, while
hydration defensively validates arbitrary implementations against the record
and rolls the retained body back on mismatch. Runtime binding snapshots the
interface Body getter once for validation, assignment, and state mutation.
`GpuWorldState`
rebuckets atomically and commits spatial visibility before draining its
transition FIFO; `LiveEntityRuntime` rejects delayed duplicate edges. A
rollback/rebucket inside a visibility observer therefore cannot expose a
remove/add pulse, leave stale final visibility, or reorder the final
presentation edge.
- `BSPQuery` contains the partial retail-style BSP collision dispatcher used by
the transition path.
- `TransitionTypes` carries `SpherePath`, `CollisionInfo`, `ObjectInfo`,
@ -296,7 +334,15 @@ owns spatial buckets only: register/rebucket/unregister are separate operations,
and landblock reloads reuse the same `WorldEntity` without replaying renderer or
script creation. Its canonical materialized view remains stable across pending
landblocks, while a separate visible-only view feeds radar, picking, status, and
targeting. Pickup/parent leave-world clears cell membership and pauses root
targeting. Raw server PhysicsState and the final state produced by retail's
ordered Lighting/NoDraw/Hidden side effects are stored separately.
`LiveEntityPresentationController` drains those accepted transitions only after
the renderer/effect owner is ready: Hidden suppresses the retained root's mesh,
collision, interaction, radar, and target eligibility while preserving the
logical record, scripts, particles, lights, full cell, and local identity.
Direct attached children receive retail's NoDraw mutation, and typed
Hidden/UnHide effects resolve through the live PhysicsScriptTable.
Pickup/parent leave-world clears cell membership and pauses root
movement/animation without destroying retained owners. `GameWindow` retains
storage-free typed views while its large feature loops are extracted.

View file

@ -187,9 +187,17 @@ src/AcDream.App/
│ └── Vfx/ # (already exists)
├── Net/
│ └── LiveSessionController.cs # owns WorldSession lifecycle, login/handshake, reconnect
├── Physics/
│ ├── ProjectileController.cs # canonical live-record projectile orchestration
│ ├── RemotePhysicsUpdater.cs # ordinary/Hidden remote narrow-tick integration
│ ├── RemoteTeleportController.cs # loaded/pending teleport placement ownership
│ ├── RemoteTeleportHook.cs # ordered retail teleport teardown actions
│ └── RemoteTeleportPlacement.cs # collision-seated SetPosition transition commit
├── World/
│ ├── InboundPhysicsStateController.cs # timestamps + accepted spawn snapshots
│ ├── LiveEntityRuntime.cs # shipped: logical lifetime + ServerGuid↔entity.Id translation
│ ├── LiveEntityPresentationController.cs # ordered Hidden/NoDraw/effect/collision side effects
│ ├── LiveEntityTeardown.cs # failure-isolated multi-owner lifecycle drain
│ └── ParentAttachmentState.cs # parent generations + pending ParentEvent relations
├── Interaction/
│ └── SelectionInteractionController.cs # owns WorldPicker, selection state, Use/PickUp dispatch
@ -231,7 +239,11 @@ instead of spread across `GameWindow`.
`InboundPhysicsStateController` for the nine-channel retail timestamp gates and
latest accepted immutable CreateObject snapshot, owns the canonical local ID
and optional runtime components, and separates logical registration from
spatial projection. `ParentAttachmentState` is runtime-owned and keys unresolved
spatial projection. It also retains raw PhysicsState separately from the final
state produced by retail's ordered side effects;
`LiveEntityPresentationController` projects those transitions into draw,
collision, effect, child-NoDraw, and target visibility without becoming a
second lifetime or GUID owner. `ParentAttachmentState` is runtime-owned and keys unresolved
relations by child and parent generation. `Rendering/Vfx/EntityEffectController`
owns the focused mixed F754/F755 pending FIFO, effect profiles, typed-table
resolution, and a readiness set; canonical ServerGuid-to-local-ID translation
@ -241,6 +253,35 @@ allocators fail fast instead of wrapping into another landblock's namespace.
All other non-Parent packet
families still need the future general queue tracked by divergence AD-32.
Remote teleport placement is bounded in `Physics/RemoteTeleportController`,
not `GameWindow`: it retains at most one pending request per materialized
incarnation, scopes it by the live generation and accepted PositionSequence,
and asks `RemoteTeleportPlacement` to collision-seat the current body when the
destination projection is available. `GameWindow` supplies lifecycle and
shadow-sync callbacks only; canonical identity remains in `LiveEntityRuntime`.
Failed hydration restores the captured source and delegates an
incarnation-scoped shadow restore to `LiveEntityPresentationController` while
that source is unloaded, so Hidden/UnHide and teleport never become competing
restore owners. A newer placement transfers that restore into an explicit
generation-scoped active-placement state before its rebucket visibility edge
even while Hidden. All intervening Hidden/UnHide and projection edges defer to
that owner until stable success or rollback completes; only then can it restore,
re-defer the source, or hand a Hidden result back for UnHide. The
`ILiveEntityRemotePlacementRuntime` seam keeps the complete cell/contact
handoff available across same-body runtime-wrapper replacement; replacing the
canonical body or dropping the placement contract within one incarnation is
rejected even after an operational component clear. `RemoteMotion.Body` is
constructor-owned; hydration compares pending/current wrappers directly to the
record body rather than trusting wrapper-to-wrapper equality. Binding reads an
interface Body getter once and reuses that snapshot. `GpuWorldState`
performs remove+place as one spatial rebucket,
then commits and serially drains visibility edges; `LiveEntityRuntime` filters
delayed duplicates. A rollback inside an observer cannot race the outer
destination-visible notification or expose an intermediate false pulse.
`LiveEntityTeardown` executes those independent owner callbacks to completion
and aggregates failures afterwards, so a throwing effect/plugin sink cannot
strand teleport, movement, shadow, light, or GUID-scoped state.
---
## 4. Extraction sequence — safest first

View file

@ -253,9 +253,9 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
| TS-44 | NPC UpdatePosition reconciliation (position + orientation) is SUPPRESSED while the entity is stuck (`PositionManager.GetStickyObjectId() != 0`). **#184 (2026-07-07) UNIFIED the NPC path onto the interp queue** — the retire-condition mechanism is now ported: the grounded UP routes through `CPhysicsObj::MoveOrTeleport` (Enqueue) and the per-tick catch-up SEEDS the `PositionManager::adjust_offset` chain where `StickyManager::adjust_offset` OVERWRITES it while armed (0x00555190 / 0x00555430 assigns m_fOrigin), exactly like the player-remote branch — so POSITION is now handled retail-faithfully by the compose-overwrite whether the gate is on or off. The `snapSuppressedByStick` gate is RETAINED for two residuals: (a) it suppresses the during-stick **Enqueue** so a stale server waypoint never enters the queue, and (b) it gates the **orientation** hard-snap (still outside the interp chain — retail's sticky owns facing). Bookkeeping (`LastServerPos/Time`, cell) still records; server truth reasserts on the first UP after unstick | `src/AcDream.App/Rendering/GameWindow.cs` (`OnLivePositionUpdated` NPC section, `snapSuppressedByStick` gate) | Retail's position mechanism (sticky-overwrites-interp) is now REACHABLE for NPCs (#184 gave them the interp-queue architecture); the gate remains only for orientation + during-stick Enqueue cleanliness | A stick that stays armed while ACE moves the monster far (shouldn't happen — sticks follow the target) keeps a stale orientation until unstick+next UP; bounded by the 1 s lease | `PositionManager::adjust_offset` 0x00555190; `CPhysicsObj::MoveOrTeleport` 0x00516330 (NPC UP routing, #184); retire the orientation residual in R6 |
| TS-46 | Player/remote collision spheres are passed as TWO SCALARS (radius, capsule-top height) and reconstructed by `SpherePath.InitPath` (foot center at `radius`, head center at `height radius`) — retail passes the Setup's SPHERE LIST verbatim (`CPhysicsObj::transition` 0x00512dc0 → `init_sphere(GetNumSphere, GetSphere, m_scale)`, ≤2 spheres, each origin AND radius × m_scale). With the corrected callers (0.48, 1.835 = Setup.Height) the reconstruction sits 5 mm off the dat: foot center 0.480 vs dat 0.475, head center 1.355 vs dat 1.350 (human Setup 0x02000001). **#184 Slice 3 (2026-07-07) NARROWED this: the remote de-overlap sweep now derives its scalars from the creature's OWN Setup (`GetSetupCylinder` = `setup.Radius`/`setup.Height` × ObjScale) — remotes NO LONGER use human dims regardless of Setup/scale.** RESIDUAL: (a) it is still the two-SCALAR reconstruction, not retail's ≤2-sphere LIST (lossy for creatures whose foot/head spheres differ), for both player and remotes; (b) the remote sweep's `stepUpHeight`/`stepDownHeight` stay a hardcoded 0.4 m, where retail derives them from `setup->step_up_height`/`step_down_height` (0x005180d0/0x005180f0, 0.04 m fallback, `radius×0.5` clamp) — an adjacent non-Setup divergence left for a later slice. (The pre-2026-07-06 value 1.2f put the head TOP at 1.2 m — the #137 window climb; fixed same day.) | `src/AcDream.Core/Physics/TransitionTypes.cs` (`InitPath`); `src/AcDream.App/Input/PlayerMovementController.cs` (player, human — correct as-is); `src/AcDream.App/Rendering/GameWindow.cs` (remote sweep now `GetSetupCylinder`-fed with a shapeless→human fallback) | The scalar API predates the Setup ingestion; 5 mm is below the visual/feel threshold; the remote scalars are now the creature's real (radius,height)×ObjScale, consistent with the shadow registration's entScale and the moveto/sticky radii | Marginal rε/r+ε grazes still flip on the 5 mm scalar offset; a creature whose head sphere is wider than its foot de-overlaps by the single (radius) approximation, not the true 2-sphere profile; the 0.4 m step heights are non-Setup for all movers | `CPhysicsObj::transition` 0x00512dc0; `SPHEREPATH::init_sphere` 0x0050c670 (≤2, ×m_scale); `set_description` 0x00514f40 (m_scale from wire ObjScale); retire by plumbing the full Setup sphere list into `InitPath` |
| ~~TS-45~~ | **RETIRED 2026-07-07** — the hand-rolled `SphereCollision` (forced `combinedR+1 cm` radial de-penetration + leaked `SetSlidingNormal` + always-Slid, head-sphere ignored) is REPLACED by the faithful `CSphere::intersects_sphere` family port (branch dispatcher 0x00537A80 + `step_sphere_up`/`slide_sphere`/`land_on_sphere`/`collide_with_point`/`step_sphere_down`), routing the grounded slide through the shared crease `SlideSphere` (0x00537440). Humanoid creatures collide via body Spheres, so this was the player-vs-monster crowd path; the radial de-penetration was the "can't wiggle free in a packed crowd" wedge. `SphereCollisionFamilyTests` (slide-around, block, ethereal) + `docs/research/2026-07-07-csphere-collision-family-pseudocode.md`. Residual `AP-84` (PerfectClip TOI dead in M1.5). | — | — | — | `CSphere::intersects_sphere` 0x00537A80 (pc:321678) |
| TS-43 | Remote teleport has no `teleport_hook` equivalent: retail tears down the position managers on every teleport (`CPhysicsObj::teleport_hook` 0x00514ed0 — `CancelMoveTo(0x3c)` @0x00514edf, `PositionManager::UnStick` @0x00514eee, `StopInterpolating`/`UnConstrain`); acdream's remote teleport is a bare UP hard-snap, so a stuck/chasing remote that the server teleports keeps its stick/moveto for up to the 1 s sticky lease / next UM. The LOCAL player side IS wired (R5-V3: `PlayerMovementController.SetPosition``PositionManager.UnStick`; the moveto cancel was already there via `StopCompletely`; the teleport-arrival site also fires the hook's tail — `EntityPhysicsHost.NotifyTeleported` = `TargetManager::ClearTarget` + `NotifyVoyeurOfEvent(Teleported)` @0x00514f1b-0x00514f28, which is what makes mobs stuck to the player drop their sticks on a recall) | `src/AcDream.App/Rendering/GameWindow.cs` (`OnLivePositionUpdated` — teleport freshness is gated, but no remote manager teardown) | Remote teleports are rare (recalls/summons); the sticky 1 s lease + UP hard-snaps self-correct within a second. The retail timestamp distinction now exists; Step 8 must connect its accepted-teleport disposition to the hook teardown. | A teleported-away attacker briefly steers toward its pre-teleport target from the new location (≤1 s) before the lease/next-UM corrects it | `CPhysicsObj::teleport_hook` 0x00514ed0; `SmartBox::HandleReceivedPosition` 0x00453FD0 |
| TS-47 | **NARROWED 2026-07-13** — typed routing now ports the named-retail recall/house/PK travel, age/birth, local display/location, UI persistence, AFK/consent, emote, friends, squelch/filter, and fill-components families. Retail-owned verbs outside the researched family set still fall through to ACE until individually verified. | `src/AcDream.UI.Abstractions/Panels/Chat/RetailClientCommandCatalog.cs`; `src/AcDream.App/UI/ClientCommandController.cs`; `src/AcDream.Core.Net/Messages/ClientCommandRequests.cs` | The high-use researched families have decomp pseudocode, typed actions, and conformance tests; unresearched registry entries must follow the same evidence-first path | An unported retail-owned verb can still produce ACE unknown-command output or server-specific behavior instead of its client action | `ClientCommunicationSystem` command-table construction around `0x00581A40..0x005850A0`; `docs/research/2026-07-13-retail-client-command-routing-pseudocode.md`; `docs/research/2026-07-13-retail-client-command-families-pseudocode.md` |
| TS-48 | Dragging an item onto another player honors the authoritative `DragItemOnPlayerOpensSecureTrade` option, but the option's default-true branch stops at the existing unavailable toast because the secure-trade transaction and UI are not ported. Direct player giving through `GiveObjectRequest 0x00CD` works when the option is disabled; NPC giving is complete. | `src/AcDream.App/UI/ItemInteractionController.cs` (`PlaceIn3D`, `PolicyActionMessage`); `src/AcDream.Core/Items/ItemInteractionPolicy.cs` | The player/NPC distinction and character preference are now faithful; inventing a direct gift while the option requests secure trade would be a worse behavioral divergence. Secure trade is a separate multi-party state machine beyond the starter-dungeon NPC-give slice. | With retail's default character options, an item dragged onto another player cannot be exchanged until the secure-trade subsystem lands. | `ItemHolder::AttemptPlaceIn3D @ 0x00588600`; `PlayerModule::DragItemOnPlayerOpensSecureTrade @ 0x005D31B0`; `ClientTradeSystem`; `docs/research/2026-07-13-retail-give-item-pseudocode.md` |
| TS-49 | Hidden-object availability is bridged through `TargetManager.NotifyVoyeurOfEventAndClear(ExitWorld)` because acdream has not ported retail's DetectionManager. Retail `CObjCell::hide_object` sends `LeftDetection` to detection voyeurs; acdream instead uses the existing non-Ok target update to tear down MoveTo/Sticky consumers and clear watched-role subscriptions while preserving the hidden object's own watcher role. | `src/AcDream.App/Rendering/EntityPhysicsHost.cs` (`NotifyHidden`); `src/AcDream.Core/Physics/Motion/TargetManager.cs` (`NotifyVoyeurOfEventAndClear`) | The current movement consumers already share TargetManager's status fan-out; the bridge prevents pursuit of an unavailable object without inventing a second partial detection database. | Plugins or future systems listening specifically for retail detection enter/leave events receive no `LeftDetection`; only movement/sticky target consumers observe the equivalent availability loss. | `CObjCell::hide_object @ 0x0052BE30`; retire by porting DetectionManager/CObjCell detection-voyeur delivery and routing Hidden through `LeftDetection` |
---

View file

@ -517,6 +517,7 @@ behavior. Estimated 1726 days focused work, 35 weeks calendar.
- **Missile/portal VFX campaign Step 6 implemented and independently reviewed 2026-07-14.** Pure Core now owns the retail projectile clock/integration/sweep primitive: 0.2-second catch-up quanta, 50-unit/second clamp, final-state acceleration, world-space omega, complete 3-D AlignPath, scaled Setup-local collision spheres, terrain/BSP/object continuous sweeps, exact `OBJECTINFO::missile_ignore`, self-shadow rejection, elastic/inelastic response, and correct full-cell rebasing across loaded landblocks. App live ownership and authoritative corrections remain Step 7. TS-2 is retired; ordinary missiles add PathClipped but not PerfectClip, so AP-83/AP-91 remain dormant.
- **Missile/portal VFX campaign Step 7 implemented and independently reviewed 2026-07-14.** `ProjectileController` now attaches the Step 6 body to the canonical `LiveEntityRecord` after materialization and advances arrows, bolts, and spell missiles without another GUID map, renderer registration, or stale-spawn reconstruction. It commits predicted frames through `WorldEntity.SetPosition`, republishes static effect roots, and keeps collision shadows plus canonical full-cell buckets synchronized. A predicted crossing into an unloaded bucket suspends the body and shadow in that same quantum; pending/pickup residence retains the shadow registration for exact re-entry, and hydration plus the 96-unit retail activity gate restart at the current clock without backlog. Fresh State/Vector/Position/Movement packets correct or stop the same body after retail timestamp gates; SetState reaches the canonical body before optional projectile acquisition, and a non-finite local receipt clock cannot consume first classification. Removing Missile retains ordinary active-body physics (delegating to MovementManager when present), while both owners share the body and incarnation-scoped live-record cell. CreateObject vectors are installed only at body construction, never replayed by later SetState. Delete, reset, and GUID reuse use normal logical teardown; ACE remains authoritative for impact, damage, effects, and deletion.
- **Missile/portal VFX campaign Step 8 implemented and independently reviewed 2026-07-14.** `LiveEntityRuntime` now distinguishes raw server PhysicsState from retail's side-effect-derived final state, beginning from constructor state `0x00400C08` and applying `set_state` in Lighting → NoDraw → Hidden order. `LiveEntityPresentationController` keeps Hidden objects logically alive while suppressing root mesh, collision, picking, radar, status, and new target acquisition; typed `PS_Hidden`/`PS_UnHide` plays remain DAT-driven, direct equipped children inherit NoDraw, particles/lights survive, and repeated/stale state cannot replay a transition. A normal visible CreateObject never fabricates UnHide. Effect-owner preparation is separate from pending F754/F755 replay so construction-state effects always run first. Remote fresh-teleport/cell-less placement now executes the exact ordered `teleport_hook` action bundle and a full-cell hard placement before contact/airborne routing, preventing an airborne correction from restoring the old location. Projectile Hidden state pauses the retained body without consuming its active identity or accumulating a clock backlog. TS-43 is retired; AP-69 remains because the separate 25-second/384-metre liveness cull is still unported.
**Reference docs:** `docs/research/retail-ui/00-master-synthesis.md` + slices 01-06. Every AC-specific behavior has a decompiled FUN_ / DAT_ citation.

View file

@ -471,8 +471,8 @@ include dungeons.
`PlayerDescription.Options1` preserves retail's player secure-trade
preference. See
`docs/research/2026-07-13-retail-give-item-pseudocode.md` and issue #216.
- **M2/M3 missile, effect, and portal presentation campaign (Steps 07
implemented and independently reviewed 2026-07-14)** — named-retail projectile and
- **M2/M3 missile, effect, and portal presentation campaign (Steps 08
independently reviewed 2026-07-14)** — named-retail projectile and
physics-script behavior is pinned in one oracle, the complete `PhysicsDesc`
and F754/F755 wire surfaces are parsed with retail timestamp gates, and
`LiveEntityRuntime` now separates logical lifetime from spatial rebucketing.
@ -526,8 +526,15 @@ include dungeons.
if Missile classification changes while pending; static missile effect
roots follow every predicted frame, and materialized rehydration never uses a
stale spawn cell. No impact, damage, effect, or delete event is synthesized
client-side. The remaining steps port
Hidden/UnHide portal presentation and run final hardening/visual gates.
client-side. Step 8 ports retail's constructor-state → PhysicsDesc
transition, ordered Lighting/NoDraw/Hidden state side effects, DAT-driven
typed Hidden/UnHide effects, direct-child NoDraw propagation, retained
particles/lights, mesh/collision/interaction suppression, and exact remote
`teleport_hook` teardown followed by a placement snap before the airborne
gate. Ordinary visible CreateObject does not fabricate UnHide; pending
F754/F755 packets replay only after construction-state effects. TS-43 is
retired. The remaining Step 9 is final hardening plus the two-client visual
gate.
- **L.1c local attack receive path (implemented 2026-07-11; live gate pending)**
local non-autonomous mt-0 UpdateMotion now uses retail's wholesale interpreted
funnel and action-stamp gate, so ACE's server-selected melee/missile action

View file

@ -698,8 +698,110 @@ The recall Animation's direct DAT hooks must be executed rather than replaced
with a guessed cloud. Hidden/UnHide state transitions and explicit typed
scripts remain distinct packet-driven events. A normal visible spawn alone is
not evidence for an UnHide materialization effect; the observer portal-in
sequence must be validated against its actual inbound state/effect packets
during Step 8.
sequence remains a final two-client packet/visual gate.
### 11.5 Step 8 implementation boundary
The port keeps raw wire state and side-effect-derived final state separate.
`LiveEntityPresentationController` drains accepted state transitions only after
the renderer, effect profile, and PhysicsScript owner are ready; pending mixed
F754/F755 packets replay after that construction-state drain. Hidden removes
the retained entity from drawing, collision, radar, picking, status, and new
target acquisition, but does not unregister its record, cell, script queue,
particles, lights, or canonical local ID. Attached children receive the exact
direct NoDraw mutation.
The Hidden update is a narrow tick, not a frozen entity. In
`CPhysicsObj::UpdatePositionInternal` (`0x00512C30`) Hidden skips PartArray
root-motion/animation advancement and `UpdatePhysicsInternal`, but still
composes `PositionManager::adjust_offset` and processes timed object hooks.
The surrounding `UpdateObjectInternal` (`0x005156B0`) then continues the
target, movement, and position-manager tails. A moving Hidden local or remote
therefore updates its root/cell and effect-pose snapshot without accepting
input, gravity, or ordinary body integration.
Retail `CObjCell::hide_object` (`0x0052BE30`) also sends DetectionManager
`LeftDetection`. DetectionManager is not yet ported, so TS-49 records the one
explicit adaptation: acdream sends the existing TargetManager non-Ok
availability edge to tear down MoveTo/Sticky consumers and clears the hidden
object's watched-role subscriptions.
Fresh remote `TELEPORT_TS`, or first placement while the canonical cell is
zero, follows `MoveOrTeleport` Branch A before its contact test:
```text
CancelMoveTo(0x3c)
UnStick
StopInterpolating
UnConstrain
ClearTarget + NotifyVoyeurOfEvent(Teleported)
report_collision_end
SetPosition the same body at received full cell + frame through CTransition
derive Contact/OnWalkable/contact plane and ground-transition state
```
This order is required even for an airborne packet; the ordinary airborne
no-op is only a Branch B/C decision and cannot undo a genuine teleport. A
canonical nonzero cell whose spatial projection is unavailable is classified
as cell-less for this decision: it cannot take the ordinary interpolation path
without a resident cell.
If the destination landblock is pending, `RemoteTeleportController` parks the
authoritative frame without contact, retains the original contact edge, and
waits for projection visibility. The request is scoped to live generation and
accepted PositionSequence. Every newer accepted Position packet refreshes that
same request; hydration collision-seats only the latest sequence, while delete,
reset, or GUID reuse discards the old incarnation's placement.
`SetPositionInternal` captures the old Contact/OnWalkable flags at entry. It
writes destination Contact while retaining the source OnWalkable bit for the
first `calc_acceleration`, then invokes `set_on_walkable` (therefore
HitGround/LeaveGround), and only then calls `handle_all_collisions` at
`0x005154FE`. Retaining source walkability through that first calculation
matters for a grounded source that hydrates onto a steep contact: retail clears
grounded angular drift before installing the destination's non-walkable state.
The callback may change velocity, so collision response must consume that
post-callback velocity while still receiving the captured source flags. This
ordering is shared by loaded teleports and local/remote Hidden PositionManager
movement. A Hidden projectile that shares the same RemoteMotion body also uses
this narrow manager tick once; projectile integration remains paused.
If projection hydration occurs but placement resolution fails, the visibility
edge cannot be treated as a retry clock. The controller restores the captured
source body frame, full cell, contact state, and projection, then drops the
pending request. A resident source restores its shadow immediately; an unloaded
source delegates one generation-scoped restore to
`LiveEntityPresentationController`, the same owner used by Hidden/UnHide, until
that projection returns. Before any newer accepted placement rebuckets, it
transfers the deferred restore into an explicit active-placement generation,
including while Hidden. That ownership remains active for the complete pending
lifetime, so any number of intervening Hidden/UnHide or visibility edges cannot
restore an unresolved pose. Only the controller's stable completion clears it:
the placement either restores after successful collision seating, re-defers its
captured source after failure, or hands a stable Hidden result back for UnHide.
A same-incarnation MovementManager/runtime rebind cannot replace the canonical
physics body or discard the typed placement contract. A wrapper replacement
around that same body is adopted by the pending request before hydration, so
component rebinding cannot strand active-placement ownership. Operational
component clearing removes the wrapper, not the incarnation's body identity or
placement requirement; only logical teardown releases those invariants. The
production wrapper's body is constructor-owned and immutable, and hydration
validates both pending/current wrappers against the record's captured body. A
malformed dynamic wrapper is detached and the canonical body is rolled back.
Every runtime-binding boundary reads the wrapper's Body exactly once, validates
that local snapshot, and installs the same reference into the live record.
A cell-less source withdraws the projection and publishes the final
invisible presentation edge. Spatial rebucketing removes
and places atomically, visibility transitions commit their new set first and
drain through a non-reentrant FIFO, and `LiveEntityRuntime` rejects delayed
duplicates against its last logical state. A rollback triggered by a
destination-visible callback therefore cannot expose remove/add pulses or be
overwritten by the outer notification.
A newer loaded destination that fails placement consumes the original pending
request and performs the same rollback. Logical teardown is independently
failure-isolated across presentation, effects, teleport, movement/target,
shadow, and light owners; session reset clears pending teleport state even when
another teardown callback throws.
## 12. Secondary-reference cross-checks