# 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. ## 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. ## 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. 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. 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. - 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. ## 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.