acdream/docs/research/2026-07-15-retail-portal-space-pseudocode.md

695 lines
29 KiB
Markdown

# Retail portal-space presentation pseudocode
Date: 2026-07-15
Scope: the client-side 3-D scene and timing shown while a player is between
world positions. This is presentation only. The server remains authoritative
for teleport start, destination position, world readiness, and completion.
## Retail oracle
Named Sept-2013 retail symbols:
- `gmSmartBoxUI::BeginTeleportAnimation` `0x004D6300`
- `gmSmartBoxUI::EndTeleportAnimation` `0x004D65A0`
- `gmSmartBoxUI::PostInit` `0x004D6B60`
- `gmSmartBoxUI::UseTime` `0x004D6E30`
- `CreatureMode::CreatureMode` `0x00454390`
- `CreatureMode::SetCameraDirection_Degrees` `0x00453760`
- `Frame::rotate` `0x004525B0`
- `CreatureMode::AddObject` `0x004556D0`
- `UIGlobals::Init` `0x004EE470`
- `UIGlobals::GetAnimLevel` `0x004EE540`
- `CPhysicsObj::set_sequence_animation` `0x0050F6F0`
- `SmartBox::TeleportPlayer` `0x00453910`
- `SmartBox::PlayerPositionUpdated` `0x00453870`
- `CPhysicsObj::teleport_hook` `0x00514ED0`
- `CommandInterpreter::PlayerTeleported` `0x006B32B0`
- `SmartBox::SetOverrideFovDistance` `0x00451BC0`
- `SmartBox::GetOverrideFovDistance` `0x00451BE0`
- `Render::set_vdst` `0x0054B240`
Constants were checked against static x86 disassembly. The Binary Ninja
pseudocode loses several x87 operands in `UseTime`; disassembly is authoritative
for those comparisons.
## Asset construction (`gmSmartBoxUI::PostInit`)
```text
portalSetupDid = ResolveClientEnum(enumValue=0x10000001, category=7)
teleportObj = CPhysicsObj.makeObject(portalSetupDid, noWeenie, makeParts)
portalViewport = child 0x10000436 as UIElement_Viewport
portalViewport.CreatureMode.AddObject(teleportObj)
portalViewport.CreatureMode.AddLight(DISTANT_LIGHT, intensity=2.0)
portalViewport.CreatureMode.SetLightDirection(0, (0.3, -1.9, 0.65))
portalViewport.CreatureMode.SetCameraPosition((0.24, -2.7, 0.88))
portalViewport.CreatureMode.UseSmartboxFOV()
```
`CreatureMode` starts with ambient light `(0.3, 0.3, 0.3)` and a 45-degree
default FOV, but `UseSmartboxFOV` makes this viewport inherit the active world
view's current FOV on every draw. Its identity camera frame looks along AC
`+Y`, with `+Z` up.
The portal viewport is a replacement for the world viewport. It is not a
full-screen UI overlay: normal retained UI continues to update and draw above
it.
Private retained-UI viewports also keep their own objects. In particular,
`gmPaperDollUI::RedressCreature @ 0x004A3BC0` creates
`m_pInventoryObject` only when it is absent and the SmartBox player is
available; a temporarily unavailable world player does not clear an already
constructed inventory object. `gmPaperDollUI::~gmPaperDollUI @ 0x004A3590`
removes that private object at the UI/session lifetime boundary. Therefore a
portal-space world quiescence may defer a paperdoll redress, but it must not
empty and later reconstruct the visible private paperdoll.
## Begin/end and scene visibility
```text
BeginTeleportAnimation(requestedState):
if currentState == Off:
gameViewDistance = SmartBox.GetOverrideFovDistance()
currentViewDistance = gameViewDistance
clear rotation-segment state
currentState = requestedState
transitionStart = now
play centered UI sound Sound_UI_EnterPortal
EndTeleportAnimation():
if currentState != Off:
currentState = TunnelContinue
transitionStart = now
SmartBox.SetOverrideFovDistance(disabled)
```
Portal/login/death transit begins directly in `Tunnel`. Logout begins in
`WorldFadeOut`. When a tunnel-family state first becomes visible:
```text
disable vivid target indicator
portalAnimDid = ResolveClientEnum(enumValue=0x10000002, category=7)
teleportObj.set_sequence_animation(
animation=portalAnimDid,
clearExisting=true,
lowFrame=1,
highFrame=end,
fps=40)
reset portal camera position to (0.24, -2.7, 0.88)
show portal viewport
hide world SmartBox viewport
```
At the end of `TunnelFadeOut`, retail commits the current transition view
distance, hides the portal viewport, shows the world viewport, clears the
teleport object's sequence animations, plays `Sound_UI_ExitPortal`, and enters
`WorldFadeIn`. That following state restores the ordinary game view distance.
The viewport swap is also the end of the destination-cell blocking edge, not
the end of the complete teleport protocol:
```text
TunnelFadeOut completes:
destination cells are already available
release SmartBox world blocking
hide portal viewport
show world viewport
enter WorldFadeIn
WorldFadeIn completes one second later:
send LoginComplete
clear teleport-in-progress
enter Off
```
The two edges must remain distinct in an asynchronous client. Holding world
blocking until `LoginComplete` creates a one-second clear-color/void interval
after the tunnel viewport has already been hidden. Conversely, marking the
whole teleport complete at the viewport swap loses retail's final
`WorldFadeIn` protocol state.
The asynchronous renderer may temporarily spend otherwise-idle resource
capacity on the withheld destination, but that policy belongs to the exact
reveal generation. Acdream raises only the count of small mesh/composite
uploads while keeping the ordinary byte, array, buffer, and mipmap ceilings.
The profile ends at the viewport swap above. It never changes the required
destination radius, the readiness predicate, or the resulting pixels.
## Camera motion
The camera rolls around its own AC `+Y` forward axis. It does not yaw around
world `+Z`. `SetCameraDirection_Degrees` resets the frame to identity, converts
the vector to radians, then passes `(0, angle, 0)` to `Frame::rotate`, whose
input is an axis-angle rotation vector. The forward direction therefore stays
down the portal passage while local `+Z` up rolls around it. Each segment is
independent:
```text
if now >= rotationStart + rotationDuration:
currentAngle = previousEndAngle
rotationStart = now
rotationDuration = RandomUniform(0.6, 1.8) seconds
startAngle = currentAngle
endAngle = RandomUniform(0, 360) degrees
display "In Portal Space - Please Wait..."
else:
t = (now - rotationStart) / rotationDuration
level = UIGlobals.GetAnimLevel(t) / 1024
currentAngle = startAngle + (endAngle - startAngle) * level
CreatureMode.SetCameraDirection_Degrees((0, currentAngle, 0))
```
`UIGlobals.GetAnimLevel` is a 100-entry cumulative sine table, not a
polynomial smoothstep:
```text
for i in 0..99:
sample[i] = truncate(sin(i * pi / 99) * 1024)
total = sum(sample)
running = 0
for i in 0..99:
running += sample[i]
level[i] = (running << 10) / total // integer division, 0..1024
GetAnimLevel(t):
t = clamp(t, 0, 1)
index = floor(t * 99)
return level[index]
```
## State timing (`gmSmartBoxUI::UseTime`)
Golden data constants:
```text
FadeTime = 1.0 seconds (0x007BD278)
MinContinue = 2.0 seconds (0x007BD268)
MaxContinue = 5.0 seconds (0x007BD270)
PortalFPS = 40 frames/s (0x007BD280)
EndFrame = 120
ExitWindowLow = FadeTime + 0.1 = 1.1 seconds
ExitWindowHigh = FadeTime + 0.3 = 1.3 seconds
```
```text
WorldFadeOut:
after 1 second -> TunnelFadeIn
TunnelFadeIn:
after 1 second -> Tunnel
Tunnel:
hold until SmartBox teleport-in-progress clears
EndTeleportAnimation -> TunnelContinue
TunnelContinue:
elapsed = now - transitionStart
if elapsed >= 2 seconds:
remaining = unsigned(120 - teleportObj.currentFrame) / 40
if elapsed >= 5 seconds
or 1.1 < remaining < 1.3 seconds:
-> TunnelFadeOut
TunnelFadeOut:
after 1 second:
hide/clear portal scene
show world
play exit sound
-> WorldFadeIn
WorldFadeIn:
after 1 second:
send LoginComplete
clear teleport-in-progress
-> Off
```
The narrow remaining-time window makes the one-second tunnel projection transition finish at
the end of the 120-frame animation. The five-second ceiling is retail's escape
for a missed window.
The four states whose retail enum names contain `FADE` use
`GetAnimLevel(elapsed / FadeTime)`. Instruction-level x86 inspection of
`gmSmartBoxUI::UseTime` (`0x004D7133..0x004D725F`) shows that this value feeds
only `SmartBox::SetOverrideFovDistance`. The function performs no alpha or
black-quad draw; `FADE` names the view-plane transition, not a transparency
compositor. The retained UI remains independent because only the two 3-D
viewports are switched.
## View-plane warp
Retail couples every `FADE` state to a projection transition. On begin it
captures the current SmartBox view-plane distance; for a perspective
projection this is `cot(verticalFov / 2)`. The transition endpoint is the
named constant `TRANSITION_VIEW_PLANE_DISTANCE = 0.001` at `0x007BD260`.
```text
viewPlaneLevel = GetAnimLevel(elapsed / FadeTime) / 1024
WorldFadeOut or TunnelFadeOut:
currentDistance = gameDistance
+ (0.001 - gameDistance) * viewPlaneLevel
TunnelFadeIn or WorldFadeIn:
currentDistance = 0.001
+ (gameDistance - 0.001) * viewPlaneLevel
SmartBox.SetOverrideFovDistance(true, currentDistance)
```
`Render::set_vdst` turns the distance back into projection values:
```text
verticalFov = 2 * atan(1 / currentDistance)
znear = max(0.1, currentDistance * 0.25)
```
At `0.001`, the view is nearly 180 degrees wide. Retail does not cover that
endpoint with black: it switches directly from the tunnel viewport to the
destination world at this projection, then `WorldFadeIn` expands the world
from the center as the captured game projection is restored. This is retail's
characteristic destination-world warp, independent of chase-camera movement.
`CreatureMode::Render @ 0x004529D0` changes the portal viewport's FOV through
the SmartBox path but does not install a private far clip. The resulting
`PrimD3DRender::SetFOVInternal @ 0x0059AB40` projection uses the shared
`Render::zfar` (default `4000` at `0x0081EC88`). The portal CreatureMode must
therefore inherit the active SmartBox projection's near and far planes rather
than installing a private short clip range.
### Portal viewport buffer lifecycle
The portal tunnel is rendered by `UIViewportObject::DrawContent @ 0x00695030`.
Before calling `CreatureMode::Render`, it executes:
```text
SetViewport(portal bounds)
RenderDevice.Clear(flags = 4, color = black, depth = 1.0)
CreatureMode.Render()
restore previous viewport and matrices
```
`RenderDeviceD3D::Clear @ 0x0059FD30` maps retail clear flags explicitly:
```text
flag 1 -> D3DCLEAR_TARGET
flag 2 -> D3DCLEAR_STENCIL, when a stencil buffer exists
flag 4 -> D3DCLEAR_ZBUFFER
```
Therefore the portal viewport clears **depth only**. The black color argument
is not used for flag 4. This does not preserve an earlier swap-chain image:
`Client::UseTime @ 0x00411C40` begins the complete frame through
`SceneTool::BeginScene @ 0x0043DAD0`, whose `RenderDevice.Clear(flags=7,
black, depth=1)` clears color, depth, and stencil. The later viewport clear
retains that frame's fresh black color target while resetting depth for the
portal CreatureMode.
`gmSmartBoxUI::UseTime @ 0x004D6E30` swaps visibility between the normal
SmartBox and the portal `UIElement_Viewport`. The world viewport is therefore
not redrawn behind portal space; the portal mesh is drawn over the whole-frame
black target, never over the destination world or stale tunnel history.
The faithful presentation rule is:
```text
at the beginning of every frame:
clear color, depth, and stencil to black
while the portal viewport is visible:
do not draw the normal world viewport
clear depth to 1.0
draw portal CreatureMode over this frame's black color target
```
One high-refresh presentation edge remains. `UIGlobals::GetAnimLevel` yields
1022 at table index 96, 1023 at index 97, and 1024 at indices 98 and 99. The
2013 client capture presents 1022 as the last outgoing viewport sample and
does not present the finite tunnel at 1023/1024 before switching viewports.
With the world pass suppressed, acdream runs portal space at roughly 2000 FPS
and can publish those sub-20.2 ms samples; the desktop compositor may then
hold one for a complete monitor refresh and expose the finite tunnel boundary.
The frame-rate-independent presentation adaptation is therefore:
```text
if state is WorldFadeOut or TunnelFadeOut
and GetAnimLevel(elapsed / FadeTime) > 1022:
perform the normal state transition before publishing the draw snapshot
```
This advances only the outgoing presentation edge by at most two table quanta
(about 20.2 ms) and is recorded as AD-38. Incoming fades retain the literal
timer.
### Shared sky and landscape projection during destination reveal
`SmartBox::RenderNormalMode @ 0x00453AA0` installs the active SmartBox
view-plane/FOV through `Render::set_vdst` or `Render::SetFOVRad` before calling
`LScape::draw @ 0x00506330`. `GameSky::Draw @ 0x00506FF0` then saves
`Render::zfar`, sets it to four times the active value for the sky draw, and
restores it. It does **not** install an independent sky FOV.
The destination world is first revealed at a nearly 180-degree projection.
Sky and landscape must therefore preserve the same horizontal/vertical
projection scales and off-center terms. Only the sky's near/far depth mapping
may differ. acdream's former fixed 60-degree sky projection left part of the
frame covered only by the clear/fog background while terrain used the teleport
projection; this was the brief background flash at tunnel exit.
```text
activeProjection = SmartBox view-plane projection
skyProjection = activeProjection
skyProjection.depthRange = authored sky near/far range
draw sky
draw landscape with activeProjection
```
Instruction-level checks that disambiguate the Binary Ninja x87 output:
- `SmartBox::GetOverrideFovDistance` `0x00451BE0`: when no override is active,
returns `cot(activeVerticalFov / 2)` (`0x00798088 = 0.5`,
`0x007928C0 = 1.0`).
- `gmSmartBoxUI::UseTime` `0x004D7199` and `0x004D722E`: converts the signed
table result with `0x007BD6A0 = 1/1024` and performs the two lerps above.
- `Render::set_vdst` `0x0054B240`: x87 `fpatan` computes
`atan(1 / distance)`, doubles it, and sets
`znear = max(0.1, distance * 0.25)`.
## Player placement boundary
```text
SmartBox.TeleportPlayer(position):
player.SetPositionSimple(position, slide=true)
PlayerPositionUpdated(isTeleport=true)
PlayerPositionUpdated(isTeleport=true):
clear position-update/teleport bookkeeping
player.teleport_hook()
commandInterpreter.PlayerTeleported()
SmartBox.set_viewer(player.position, reset_sought=true)
update CellManager position
player.teleport_hook():
MovementManager.CancelMoveTo(error=0x3c)
PositionManager.UnStick()
PositionManager.StopInterpolating()
PositionManager.UnConstrain()
TargetManager.ClearTarget()
TargetManager.NotifyVoyeurOfEvent(Teleported)
report_collision_end(force=true)
PlayerTeleported():
SetAutoRun(false, send=true)
SendMovementEvent()
```
The numeric `0x3c` is the internal `ITeleported` completion status, not a
player-visible error. `ClientCommunicationSystem::HandleFailureEvent`
`0x00571990` has a visible case for `0x3d` (`YouChargedTooFar`) but no case for
`0x3b` (`ILeftTheWorld`) or `0x3c`; both fall through without adding scroll
text. ACE also sends `ITeleported` after a successful portal use. acdream must
consume those two control statuses silently rather than formatting an unknown
`WeenieError` line.
The viewer reset copies the player's complete position into both `viewer` and
`viewer_sought_position`. Subsequent ordinary camera updates extend the chase
boom outward through the same damped and swept path used after camera
collision. Reusing the source-world viewer is not retail behavior.
## Recall completion versus Hidden
The retail teleport hook does not clear the character's animation sequence.
Recall completion is instead guaranteed by the enclosing frame order:
```text
Client.UseTime:
Timer.update_time()
ClientNet.UseTime()
process packet controller/cache/UI work
SmartBox.UseTime()
SmartBox.Draw()
SmartBox.UseTime:
CObjectMaint.UseTime()
CPhysics.UseTime() // advances PartArray, particles, scripts
GameTime/LScape/Ambient.UseTime()
while inbound queue not empty:
DispatchSmartBoxEvent() // SetState/Hidden lands here
CommandInterpreter.UseTime() // ShouldSendPositionEvent -> AP
```
The exact call sites are `Client::UseTime` `0x00411C40` and
`SmartBox::UseTime` `0x00455410`, and `CommandInterpreter::UseTime`
`0x006B3BF0`. `SmartBox::DoSetState` `0x004520D0` applies the accepted
state through `CPhysicsObj::set_state` `0x00514DD0`. Separately,
`CPhysicsObj::UpdatePositionInternal` `0x00512C30` skips `CPartArray::Update`
while Hidden, so ordinary animation never advances behind the portal viewport.
### Destination simulation resumes before the portal viewport exits
The retail destination has two different release edges. They must not share
one acdream gate:
```text
SmartBox::DoSetState(player, visible):
set_state(...)
waiting_for_teleport = false
SmartBox::UseTime(next frame):
if not cell_manager.blocking_for_cells:
position_update_complete = true
CObjectMaint::UseTime()
CPhysics::UseTime()
gmSmartBoxUI::UseTime:
if SmartBox::teleport_in_progress() changed from true to false:
EndTeleportAnimation()
gmSmartBoxUI::EndTeleportAnimation @ 0x004D65A0:
teleportAnimState = TAS_TUNNEL_CONTINUE
teleportTransitionStartTime = Timer::cur_time
clear temporary FOV override
gmSmartBoxUI::UseTime, at least two seconds later:
TAS_TUNNEL_CONTINUE -> TAS_TUNNEL_FADE_OUT
...
TAS_TUNNEL_FADE_OUT -> TAS_WORLD_FADE_IN
hide portal viewport
show SmartBox world viewport
```
Therefore `TAS_TUNNEL_CONTINUE` is not a continued cell-loading freeze. The
destination world is already ticking behind the private portal viewport for at
least two seconds. That hidden interval is when the player pose, queued
Hidden/UnHide PhysicsScripts, attached emitters, and destination presentation
settle. Only the later `TUNNEL_FADE_OUT -> WORLD_FADE_IN` edge changes which
viewport is drawn.
Holding acdream's world-generation quiescence until the viewport swap is a
behavioral error: the first destination physics/effect tick occurs on the
first visible world frame, producing a character pop plus leftover recall or
purple materialization effects. The faithful split is:
- end world simulation/audio quiescence when the accepted destination is
materialized and `TunnelContinue` begins;
- retain the destination streaming/resource reservation and keep drawing only
the portal viewport until the later viewport-swap edge;
- keep LoginComplete on the final one-second `WorldFadeIn -> Off` edge.
ACE schedules lifestone recall teleport after the MotionTable-reported action
length. With the installed human DATs, the boundary is about 15.06024 seconds;
floating-point conversion can leave acdream at frame 149.999998 of the
0..149 recall node if it dispatches Hidden before the current object tick. On
UnHide that microscopic remainder becomes a visible destination-side recall
tail if Hidden only suppresses drawing. Retail has a second, decisive boundary:
`CPhysicsObj::set_hidden` `0x00514C60` invokes
`CPartArray::HandleEnterWorld` `0x00517D70` on **both** Hidden and UnHide.
That delegates to `MotionTableManager::HandleEnterWorld` `0x0051BDD0`, which
removes every link animation from the sequence and drains every pending
completion as aborted. On the Hidden edge it runs after the Hidden PES, child
NoDraw propagation, collision stop, and cell hide. On UnHide it runs after
collision restoration and before the cell becomes visible. A casting recoil,
recall remainder, or authored stance transition therefore cannot resume after
portal space.
ACE also has timing differences when recall starts in combat. Its
`Player_Location` handlers call `SetCombatMode(NonCombat)` and immediately send
the NonCombat/Ready/recall command state. House, Lifestone, Allegiance Hometown,
and PK Arena schedule `Teleport` using only
`MotionTable.GetAnimationLength(recall)`. Retail
`MovementManager::unpack_movement` (`0x00524440`) and
`CMotionInterp::move_to_interpreted_state` (`0x005289C0`) preserve the authored
combat-to-NonCombat link before the recall action. For a human leaving Magic
stance that link is about 0.31 seconds, but ACE does not add it to the teleport
delay. The retail Hidden boundary intentionally tolerates that: whichever
authored links remain when teleport begins are retired by `set_hidden`, not
carried through the tunnel.
Marketplace is a separate ACE residual: its handler deliberately uses a fixed
14-second delay and comments out `GetAnimationLength` because the installed
animation is approximately 18.4 seconds. That can shorten the visible source-
side action, but the client must still apply the same retail Hidden teardown;
no Marketplace-specific classifier or portal-exit behavior is required.
## acdream integration translation
- A dedicated App-layer portal presentation owns the synthetic portal object,
animation sequence, camera, scene light, resource references, and draw pass.
- The existing pure teleport state machine remains the lifecycle owner, but it
consumes the portal object's current frame for retail exit timing and uses
the exact table-driven easing curve.
- During tunnel states the portal scene replaces world pixels before retained
UI draws. There is no black compositor. Input dispatch and
`RetailUiRuntime.Tick` continue unchanged.
- The projection owner captures the active camera's M22 view-plane distance at
teleport begin and applies `Render::set_vdst` semantics to both the tunnel
and world viewport during the four `FADE` states.
- F751 remains a notification gate and does not itself advance `TELEPORT_TS`.
Presentation correlates its sequence with exactly one accepted destination
Position. A Position that arrives before F751 is buffered only when it
advances that timestamp; after F751, the first accepted matching Position is
consumed even if the channel already advanced. Thus reordered delivery and
retransmitted notifications cannot materialize the wrong destination or
strand the transit, while canonical physics retains its own retail gates.
### Runtime F751/Position ownership pseudocode (Slice J6.3)
Named retail `SmartBox::HandlePlayerTeleport @ 0x00452150` compares the
incoming F751 sequence with the player's current `update_times[4]`
(`TELEPORT_TS`) using the retail unsigned-16 wrap rule. An older notification
is rejected. Equality or a newer notification clears
`position_update_complete` and `has_been_teleported`, then sets
`waiting_for_teleport`; it does **not** write `update_times[4]`.
`SmartBox::TeleportPlayer @ 0x00453910` later installs the complete accepted
`Position` and calls `PlayerPositionUpdated`. ACE corroborates the wire
transaction: `GameMessagePlayerTeleport` advances ObjectTeleport once, then
the immediately following UpdatePosition carries that same value. Holtburger
confirms F751 is only one little-endian `u16` plus alignment.
The Runtime ownership port therefore keeps canonical physics admission and
transit correlation separate:
```text
on F751(sequence):
reject unless canonical player TELEPORT_TS admits equality/newer
reject if this Runtime transit owner already observed sequence or a newer one
retire buffered destinations older than sequence
end the prior active correlation, retaining wrap-aware history
remember sequence as a pending teleport start
when the graphical host can enter portal presentation:
promote pending sequence to active sequence
if an accepted matching Position was buffered:
expose it as the one destination waiting to be aimed
on accepted local-player Position(destination, teleportAdvanced):
canonical physics has already admitted and applied the packet
if teleportAdvanced:
retire buffered destinations older than destination.TeleportSequence
if active sequence equals destination.TeleportSequence:
accept only the first destination
else if pending sequence equals destination.TeleportSequence:
buffer only the first destination until presentation activates
else if teleportAdvanced and the sequence may belong to a future F751:
buffer only the first destination
else:
ignore it for transit correlation
when App aims a destination:
Runtime atomically verifies:
active teleport
exact teleport sequence
exact accepted destination cell
Runtime creates the one reveal generation and consumes the aim claim
App translates the cell-local frame into render space and recenters
before and after App placement:
verify the same reveal generation + teleport sequence + destination cell
only then acknowledge Runtime materialization
on ordinary portal completion:
end active correlation but retain last-F751 history
on session reset:
clear pending/active correlation, buffered/accepted destinations,
last-F751 history, and the visible reveal snapshot
retain the monotonic reveal-generation counter
```
The Position-before-F751 buffer is not a second position authority. It stores
only immutable data from a Position already accepted by the canonical Runtime
physics owner, and exists because host projection availability and ordered
event delivery can straddle the presentation activation edge. It can neither
apply physics nor turn an ordinary non-advancing Position into a portal
destination.
- Destination placement resets the retail chase camera's published and sought
positions to the player before the normal update path resumes.
- `RetailLiveFrameCoordinator` owns the `SmartBox::UseTime` phase barrier:
local/remote object and projectile advancement, final pose/hook publication,
attached emitters/lights, particle simulation, and PhysicsScripts all precede
inbound session dispatch in that retail order; rendering consumes the resulting
coherent snapshot.
- `RetailLocalPlayerFrameController` owns the local player's exactly-once
object tick. Input-originated movement and jump packets are serialized from
its pre-network result; F751 or ForcePosition received later in the frame
cannot relocate or replay those one-shot commands. After inbound dispatch,
`CommandInterpreter::UseTime @ 0x006B3BF0` evaluates AutonomousPosition from
the current controller frame, matching its exact retail slot.
- After inbound dispatch, a non-time-advancing reconciliation composes equipped
children and refreshes emitter/light anchors from the authoritative root. It
does not rerun animation, scripts, particles, or fades. A player first created
by that inbound pass is likewise projected without receiving a late physics
tick; its first normal object tick occurs on the next update.
This is update-loop ordering, not portal-specific animation logic.
- `LiveEntityPresentationController` owns the complete accepted Hidden/UnHide
side-effect order. Both edges route the current local entity to its existing
`AnimationSequencer.Manager.HandleEnterWorld`, matching retail's PartArray
boundary for player, creature, and object animations without classifying the
preceding action.
- Hidden/UnHide typed scripts use retail's internal path, not ordinary
`CPhysicsObj::play_script` (`0x00513260`). `set_hidden` resolves the current
PhysicsScriptTable and calls `play_script_internal` even when the object has
no current cell. The serial script remains paused until destination re-entry;
this preserves table `0x34000004`'s purple Hidden emitters across asynchronous
portal placement. Ordinary F755/default-hook playback retains the cell gate.
- Destination residency remains acdream's asynchronous adaptation. It supplies
the same `EndTeleportAnimation` edge retail receives when its blocking cell
load completes; it does not alter presentation ordering.
- The portal object is synthetic and never enters `LiveEntityRuntime`, world
picking, collision, radar, or server GUID ownership.
## Modern-runtime host projection mapping (Slice J6)
The accepted retail sequence remains authoritative; Slice J6 changes only
which process-local owner proves each edge:
```text
Runtime accepts exact generation + destination
App prepares destination resources
App acknowledges typed readiness to Runtime
Runtime advances materialization / simulation availability
App projects that exact generation into graphical state
App acknowledges simulation release
App releases the exact destination reservation
App acknowledges reservation release
App observes the normal viewport for that generation
App acknowledges terminal projection
Runtime may retire the completed host record
```
The Runtime token includes both generation and destination cell. A callback
failure leaves the exact unacknowledged suffix live; a superseding portal
cannot consume it. This is ownership bookkeeping around the existing retail
order, not a timer, asynchronous UI queue, or alternate portal behavior.
## Cross-reference notes
- `references/WorldBuilder` supplies the extracted setup/GfxObj mesh and modern
draw pipeline used by the synthetic scene; it has no teleport lifecycle.
- Holtburger's network study (`docs/research/2026-05-10-holtburger-network-stack-study.md`)
corroborates the LoginComplete wire acknowledgement, but intentionally has no
retail UI/CreatureMode implementation. The named retail client is therefore
the presentation oracle.