acdream/docs/research/2026-07-15-retail-portal-space-pseudocode.md
Erik eab23cbdd1 feat(world): port retail portal-space viewport
Replace the opaque teleport cover with the DAT-authored CreatureMode tunnel, exact retail camera/light/animation timing and easing, and animation-hook audio routing. Compose portal and fade passes below retained UI so gameplay windows and input remain active through F751 travel.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-15 21:59:23 +02:00

8.1 KiB

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

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)

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

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:

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 restores the world view distance, hides the portal viewport, shows the world viewport, clears the teleport object's sequence animations, plays Sound_UI_ExitPortal, and enters WorldFadeIn.

Camera motion

The camera rotates around AC's vertical +Z axis. Each segment is independent:

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:

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:

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
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 fade finish at the end of the 120-frame animation. The five-second ceiling is retail's escape for a missed window.

World/tunnel fades use GetAnimLevel(elapsed / FadeTime). The fade belongs between the 3-D viewport and the retained UI. It must never cover or disable the retained UI.

Player placement boundary

SmartBox.TeleportPlayer(position):
    player.SetPositionSimple(position, slide=true)
    PlayerPositionUpdated(isTeleport=true)

PlayerPositionUpdated(isTeleport=true):
    clear position-update/teleport bookkeeping
    player.teleport_hook()
    commandInterpreter.PlayerTeleported()
    move world viewer to player
    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 retail teleport hook does not clear the character's animation sequence. The arrival presentation is instead hidden by the portal scene and its final world fade while the normal motion/update stream settles. Therefore acdream must not add an arrival-only animation reset.

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. The black fade also draws before retained UI. Input dispatch and RetailUiRuntime.Tick continue unchanged.
  • 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.