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>
243 lines
8.1 KiB
Markdown
243 lines
8.1 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`
|
|
- `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.
|