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:
parent
0eab7497c1
commit
eab23cbdd1
14 changed files with 983 additions and 45 deletions
243
docs/research/2026-07-15-retail-portal-space-pseudocode.md
Normal file
243
docs/research/2026-07-15-retail-portal-space-pseudocode.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue