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

@ -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