fix(teleport): preserve dungeon cell identity on respawn (#215)
Classify teleport landblock transitions from the source and destination Position cell IDs instead of render-space coordinates. This keeps same-dungeon death respawns on their resident floor physics while preserving the existing true cross-landblock streaming path. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
b0c175afc0
commit
0ad6700a07
7 changed files with 296 additions and 9 deletions
|
|
@ -0,0 +1,115 @@
|
|||
# Same-dungeon respawn landblock identity — retail pseudocode
|
||||
|
||||
## Symptom and captured cause
|
||||
|
||||
After `/die` in the starter dungeon, ACE respawned the player in the same cell,
|
||||
`0x8C0401AD`. The player stood correctly until the first movement input, then
|
||||
lost floor contact and fell through the world.
|
||||
|
||||
The destination update and the player controller both named cell
|
||||
`0x8C0401AD`, but acdream reconstructed the source landblock from render-space
|
||||
XYZ. The dungeon cell's valid local Y was about `-30.4 m`, so this expression:
|
||||
|
||||
```text
|
||||
sourceLbY = streamingCenterY + floor(renderY / 192 m)
|
||||
= 4 + floor(-30.4 / 192)
|
||||
= 3
|
||||
```
|
||||
|
||||
misidentified the source as landblock `(140,3)`. The destination was `(140,4)`,
|
||||
so the same-cell respawn entered the cross-landblock branch, removed
|
||||
`0x8C04FFFF` from the physics engine, and left the player with no hydrated floor.
|
||||
|
||||
Dungeon EnvCells may have arbitrary negative frame origins. Their XYZ values
|
||||
therefore cannot identify an AC landblock.
|
||||
|
||||
## Named retail oracle
|
||||
|
||||
Retail stores cell identity inside every position:
|
||||
|
||||
```c
|
||||
// acclient.h:30658-30662
|
||||
struct Position : PackObj
|
||||
{
|
||||
unsigned int objcell_id;
|
||||
Frame frame;
|
||||
};
|
||||
```
|
||||
|
||||
The local-player teleport path preserves that complete value end to end:
|
||||
|
||||
- `SmartBox::TeleportPlayer @ 0x00453910` passes its `Position const*` directly
|
||||
to `CPhysicsObj::SetPositionSimple`.
|
||||
- `CPhysicsObj::SetPositionSimple @ 0x005162B0` copies that complete `Position`
|
||||
into a `SetPositionStruct`, then calls `CPhysicsObj::SetPosition`.
|
||||
- `SmartBox::HandleReceivedPosition`'s local-player teleport branch
|
||||
(`0x0045415F`–`0x00454168`) passes the received `Position` to
|
||||
`SmartBox::TeleportPlayer`; it does not derive a cell from XYZ.
|
||||
|
||||
References:
|
||||
|
||||
- `docs/research/named-retail/acclient.h:30658`
|
||||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt:92514`
|
||||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt:93013`
|
||||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt:284276`
|
||||
|
||||
## Reference-client/server cross-check
|
||||
|
||||
The existing L.3 research traces the same contract through two independent
|
||||
implementations:
|
||||
|
||||
- ACE sends death respawn as `GameMessagePlayerTeleport`, immediately followed
|
||||
by an `UpdatePosition` whose `PositionPack` contains the destination cell and
|
||||
frame (`docs/research/2026-05-04-l3-port/12-hard-teleport-branch.md`,
|
||||
sections 7 and 9).
|
||||
- The retail packet path unpacks `PositionPack.position` as
|
||||
`objcell_id + Frame` and routes that complete value into the hard-teleport
|
||||
branch (`docs/research/2026-05-04-l3-port/03-up-routing.md`, sections 0 and 3).
|
||||
|
||||
The repository's old external ACE checkout is no longer present after the
|
||||
WorldBuilder extraction, so the checked-in cross-reference research is the
|
||||
stable ACE source citation for this change.
|
||||
|
||||
## Faithful pseudocode
|
||||
|
||||
Retail has synchronous cell access and can install the received `Position`
|
||||
directly. acdream keeps its asynchronous loading cover, but the identity
|
||||
decision must use the same `Position.objcell_id` values:
|
||||
|
||||
```text
|
||||
on local player teleport destination(receivedPosition):
|
||||
sourceCell = player.currentPosition.objcell_id
|
||||
destinationCell = receivedPosition.objcell_id
|
||||
|
||||
if sourceCell is zero:
|
||||
# acdream-only pre-placement fallback; no XYZ inference
|
||||
sourceLandblock = currentStreamingCenterLandblock
|
||||
else:
|
||||
sourceLandblock = landblockOf(sourceCell)
|
||||
|
||||
destinationLandblock = landblockOf(destinationCell)
|
||||
crossesLandblock = sourceLandblock != destinationLandblock
|
||||
|
||||
if crossesLandblock:
|
||||
evict the old streaming-origin physics landblock
|
||||
recenter streaming on destinationLandblock
|
||||
start the existing destination hydration path
|
||||
|
||||
retain receivedPosition.objcell_id as the pending destination cell
|
||||
when the existing readiness gate opens:
|
||||
place the player with the complete destination cell + frame
|
||||
```
|
||||
|
||||
`landblockOf(cell)` means `(cell & 0xFFFF0000) | 0xFFFF`. At no point does
|
||||
the decision divide or floor position coordinates.
|
||||
|
||||
## Conformance cases
|
||||
|
||||
1. Source and destination are both `0x8C0401AD`, while render Y is negative:
|
||||
same landblock; do not evict or recenter.
|
||||
2. Source and destination are different EnvCells in `0x8C04`:
|
||||
same landblock; do not evict or recenter.
|
||||
3. Source `0x8C0401AD`, destination `0xA9B40022`:
|
||||
cross-landblock; retain the existing recenter/hydration behavior.
|
||||
4. Source cell is zero before first placement:
|
||||
compare against the current streaming-center ID; never infer from XYZ.
|
||||
Loading…
Add table
Add a link
Reference in a new issue