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>
This commit is contained in:
Erik 2026-07-15 21:59:23 +02:00
parent 0eab7497c1
commit eab23cbdd1
14 changed files with 983 additions and 45 deletions

View file

@ -0,0 +1,243 @@
# 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`)
```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 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:
```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 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
```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()
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.