fix(G.3): place the player on the cell floor for an indoor dungeon login (#135 follow-up)
Two regressions from the pre-collapse (712f17f), found by live gate + a runtime
probe:
1) Login-into-dungeon stopped loading the dungeon. The login-hold streaming
observer fell through to the OFFLINE fly-camera branch once
_lastLivePlayerLandblockId was filtered to the player guid (a dungeon-local
NPC used to keep it pinned). A camera-derived observer far from the
pre-collapsed dungeon tripped ExitDungeonExpand and unloaded it. Fix: a LIVE
in-world session never uses the fly camera for the observer — it follows the
player's server landblock, falling back to the recentered spawn center
(_liveCenterX/Y). The fly camera is the OFFLINE observer only.
2) Even with the dungeon resident, auto-entry hung: the #106 "ground ready" gate
required SampleTerrainZ under the spawn, but a dungeon's negative-offset cells
place the spawn's WORLD position in a NEIGHBOUR terrain landblock the #135
collapse deliberately doesn't load (probe: cellReady=True, terrReady=False
forever). The terrain gate is wrong for an indoor spawn — the player lands on
the EnvCell FLOOR. Fix: gate an indoor (hydratable) spawn/teleport on
IsSpawnCellReady, not the terrain heightmap; outdoor (and unhydratable→demote)
spawns still hold on terrain. Applied to both isSpawnGroundReady (login auto-
entry) and TeleportArrivalReadiness (teleport). This is the faithful equivalent
of retail's synchronous cell load + place-on-floor; the pre-#135 terrain hold
only passed because the 25x25 window streamed the neighbour terrain.
Verified live: login into 0x0007 → auto-entered player mode, snapped to
0x00070145, dungeon renders, FPS steady. Register AD-2 amended.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
712f17f0f2
commit
2c923755c4
2 changed files with 64 additions and 24 deletions
|
|
@ -1007,21 +1007,36 @@ public sealed class GameWindow : IDisposable
|
|||
// integrates gravity against an empty world and free-falls
|
||||
// the player into the void (retail loads cells synchronously;
|
||||
// this is the async-streaming equivalent of that invariant).
|
||||
isSpawnGroundReady: () => _entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe)
|
||||
&& _physicsEngine.SampleTerrainZ(pe.Position.X, pe.Position.Y) is not null
|
||||
// #107 gate-2 extension (2026-06-10): an INDOOR spawn claim
|
||||
// additionally waits for the claimed cell's hydration so the
|
||||
// entry snap's AdjustPosition validation can act (retail loads
|
||||
// the cell synchronously before SetPosition; this is the
|
||||
// async-streaming equivalent). Claims that can never hydrate
|
||||
// (id outside the landblock's NumCells range per the dat)
|
||||
// don't hold the gate — the Resolve-head safety net demotes
|
||||
// them loudly.
|
||||
&& (!_lastSpawnByGuid.TryGetValue(_playerServerGuid, out var sp)
|
||||
|| sp.Position is not { } spawnClaim
|
||||
|| spawnClaim.LandblockId == 0
|
||||
|| _physicsEngine.IsSpawnCellReady(spawnClaim.LandblockId)
|
||||
|| IsSpawnClaimUnhydratable(spawnClaim.LandblockId)),
|
||||
isSpawnGroundReady: () =>
|
||||
{
|
||||
if (!_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe)) return false;
|
||||
|
||||
// #107 / #135: spawn-ground readiness is spawn-claim aware. For an
|
||||
// INDOOR claim (sealed dungeon / building interior) the ground the
|
||||
// player lands on is the EnvCell FLOOR (its BSP), so gate on the
|
||||
// cell's hydration (IsSpawnCellReady) — NOT the terrain heightmap.
|
||||
// A dungeon's cells sit in their landblock at an arbitrary (often
|
||||
// negative) offset, so the spawn's WORLD position can fall in a
|
||||
// NEIGHBOUR terrain landblock that the #135 dungeon collapse
|
||||
// deliberately does not load; requiring terrain there hangs login
|
||||
// forever (cellReady true, SampleTerrainZ null). Retail loads the
|
||||
// cell synchronously and places the player on the cell floor —
|
||||
// cellReady is the faithful indoor equivalent (#106/#107, AD-2).
|
||||
// (Before #135 this only passed by accident: the 25×25 window
|
||||
// happened to stream the neighbour terrain.)
|
||||
if (_lastSpawnByGuid.TryGetValue(_playerServerGuid, out var sp)
|
||||
&& sp.Position is { } spawnClaim
|
||||
&& spawnClaim.LandblockId != 0
|
||||
&& (spawnClaim.LandblockId & 0xFFFFu) >= 0x0100u
|
||||
&& !IsSpawnClaimUnhydratable(spawnClaim.LandblockId))
|
||||
return _physicsEngine.IsSpawnCellReady(spawnClaim.LandblockId);
|
||||
|
||||
// Outdoor spawn, OR an unhydratable indoor claim that will demote to
|
||||
// an outdoor position: hold until the terrain under the spawn streams
|
||||
// (the original #106 gate — entering against an empty world free-falls
|
||||
// the player into the void).
|
||||
return _physicsEngine.SampleTerrainZ(pe.Position.X, pe.Position.Y) is not null;
|
||||
},
|
||||
enterPlayerMode: EnterPlayerModeFromAutoEntry);
|
||||
}
|
||||
|
||||
|
|
@ -5013,10 +5028,19 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
if (IsSpawnClaimUnhydratable(destCell))
|
||||
return AcDream.App.World.ArrivalReadiness.Impossible;
|
||||
if (_physicsEngine.SampleTerrainZ(destPos.X, destPos.Y) is null)
|
||||
return AcDream.App.World.ArrivalReadiness.NotReady;
|
||||
|
||||
// #135: an INDOOR destination (sealed dungeon / building interior) gates on the
|
||||
// EnvCell FLOOR, not the terrain heightmap. A dungeon's negative-offset cells can
|
||||
// place destPos in a NEIGHBOUR terrain landblock the #135 collapse doesn't load,
|
||||
// so SampleTerrainZ would stay null forever (the cell IS ready). Retail places on
|
||||
// the cell floor. Outdoor: the terrain heightmap is the ground.
|
||||
bool indoor = (destCell & 0xFFFFu) >= 0x0100u;
|
||||
if (indoor && !_physicsEngine.IsSpawnCellReady(destCell))
|
||||
if (indoor)
|
||||
return _physicsEngine.IsSpawnCellReady(destCell)
|
||||
? AcDream.App.World.ArrivalReadiness.Ready
|
||||
: AcDream.App.World.ArrivalReadiness.NotReady;
|
||||
|
||||
if (_physicsEngine.SampleTerrainZ(destPos.X, destPos.Y) is null)
|
||||
return AcDream.App.World.ArrivalReadiness.NotReady;
|
||||
return AcDream.App.World.ArrivalReadiness.Ready;
|
||||
}
|
||||
|
|
@ -6929,12 +6953,28 @@ public sealed class GameWindow : IDisposable
|
|||
observerCy = _liveCenterY + (int)System.Math.Floor(pp.Y / 192f);
|
||||
}
|
||||
else if (_liveSession is not null
|
||||
&& _liveSession.CurrentState == AcDream.Core.Net.WorldSession.State.InWorld
|
||||
&& _lastLivePlayerLandblockId is { } lid)
|
||||
&& _liveSession.CurrentState == AcDream.Core.Net.WorldSession.State.InWorld)
|
||||
{
|
||||
// Live mode (fly camera): follow the server's last-known player position.
|
||||
observerCx = (int)((lid >> 24) & 0xFFu);
|
||||
observerCy = (int)((lid >> 16) & 0xFFu);
|
||||
// Live, not yet in player mode: the login auto-entry hold, or a live
|
||||
// fly-camera spectator. Follow the PLAYER's server-known landblock; if it
|
||||
// hasn't arrived yet, KEEP the _liveCenterX/_liveCenterY default — which is
|
||||
// the spawn/teleport recenter (the dungeon landblock at a dungeon login).
|
||||
//
|
||||
// #135 regression fix (2026-06-14): this MUST NOT fall through to the
|
||||
// fly-camera projection below. During a dungeon-login hold the streaming is
|
||||
// pre-collapsed onto the spawn landblock; a camera-derived observer far from
|
||||
// it trips ExitDungeonExpand and unloads the dungeon before it can hydrate —
|
||||
// the player is never placed and login hangs with no dungeon. Previously
|
||||
// _lastLivePlayerLandblockId was set by ANY entity, so a dungeon-local NPC
|
||||
// kept this branch on the dungeon; once it was filtered to the player guid
|
||||
// (line ~4507), a not-yet-arrived player UP dropped to the camera branch.
|
||||
// The fly camera is the OFFLINE observer only.
|
||||
if (_lastLivePlayerLandblockId is { } lid)
|
||||
{
|
||||
observerCx = (int)((lid >> 24) & 0xFFu);
|
||||
observerCy = (int)((lid >> 16) & 0xFFu);
|
||||
}
|
||||
// else: keep the _liveCenterX/_liveCenterY default (the spawn recenter).
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue