feat(G.3a): hold teleport arrival until dungeon hydrates, then place (#133)
Replaces the unconditional OnLivePositionUpdated snap (which resolved against the resident old landblocks before the destination streamed in -> ocean) with a recenter + deferred BeginArrival; per-frame Tick places via the unchanged #111 validated-claim Resolve once SampleTerrainZ + IsSpawnCellReady report ready, or force-snaps loudly on an impossible claim / ~10s timeout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aca4b4645a
commit
f22121bd7d
1 changed files with 106 additions and 69 deletions
|
|
@ -4874,7 +4874,7 @@ public sealed class GameWindow : IDisposable
|
|||
entity.Rotation = rmState.Body.Orientation;
|
||||
}
|
||||
|
||||
// Phase B.3: portal-space arrival detection.
|
||||
// Phase B.3 / G.3a (#133): portal-space arrival detection.
|
||||
// Only runs for our own player character while in PortalSpace.
|
||||
if (_playerController is not null
|
||||
&& _playerController.State == AcDream.App.Input.PlayerState.PortalSpace
|
||||
|
|
@ -4888,79 +4888,109 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
bool differentLandblock = (lbX != oldLbX || lbY != oldLbY);
|
||||
|
||||
// #107 (2026-06-10): ANY player position update while in PortalSpace
|
||||
// IS the teleport arrival. Retail/holtburger exit portal space on the
|
||||
// next position event unconditionally (holtburger messages.rs
|
||||
// PlayerTeleport handler: log + LoginComplete; the destination applies
|
||||
// through the normal position flow — no distance test). The old
|
||||
// `differentLandblock || farAway(>100m)` arrival gate was an
|
||||
// invention: ACE's same-landblock short-hop position corrections
|
||||
// (e.g. right after an indoor login) matched neither condition, so
|
||||
// PortalSpace never exited and movement input stayed frozen for the
|
||||
// whole session (the #107 "input ignored" wedge shape —
|
||||
// flood-fix-gate2.log: `teleport started (seq=1)` with no arrival).
|
||||
Console.WriteLine(
|
||||
$"live: teleport arrival — old lb=({oldLbX},{oldLbY}) " +
|
||||
$"new lb=({lbX},{lbY}) dist={System.Numerics.Vector3.Distance(worldPos, oldPos):F1}");
|
||||
|
||||
System.Numerics.Vector3 newWorldPos;
|
||||
if (differentLandblock)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"live: teleport arrival — old lb=({oldLbX},{oldLbY}) " +
|
||||
$"new lb=({lbX},{lbY}) dist={System.Numerics.Vector3.Distance(worldPos, oldPos):F1}");
|
||||
|
||||
System.Numerics.Vector3 newWorldPos;
|
||||
if (differentLandblock)
|
||||
{
|
||||
// 1. Recenter the streaming controller on the new landblock.
|
||||
_liveCenterX = lbX;
|
||||
_liveCenterY = lbY;
|
||||
|
||||
// Recompute worldPos with new center (it becomes local-to-center).
|
||||
// After recentering, the new position is (p.PositionX, p.PositionY, p.PositionZ)
|
||||
// relative to the new origin — which maps to world-space (0,0,0) + local offset.
|
||||
// The streamingController.Tick will pick up _liveCenterX/_liveCenterY automatically.
|
||||
newWorldPos = new System.Numerics.Vector3(p.PositionX, p.PositionY, p.PositionZ);
|
||||
// (after recentering, origin is (0,0,0) since lb == center)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Same landblock: worldPos is already in the current center frame.
|
||||
newWorldPos = worldPos;
|
||||
}
|
||||
|
||||
// 2. Resolve through physics for the correct ground Z.
|
||||
uint newCellId = p.LandblockId;
|
||||
var resolved = _physicsEngine.Resolve(
|
||||
newWorldPos, newCellId,
|
||||
System.Numerics.Vector3.Zero, _playerController.StepUpHeight);
|
||||
var snappedPos = new System.Numerics.Vector3(
|
||||
resolved.Position.X, resolved.Position.Y, resolved.Position.Z);
|
||||
|
||||
// 3. Snap player entity + controller.
|
||||
entity.SetPosition(snappedPos);
|
||||
entity.ParentCellId = resolved.CellId;
|
||||
entity.Rotation = rot;
|
||||
_playerController.SetPosition(snappedPos, resolved.CellId);
|
||||
|
||||
// 4. Recenter chase camera on the new position.
|
||||
_chaseCamera?.Update(snappedPos, _playerController.Yaw);
|
||||
_retailChaseCamera?.Update(snappedPos, _playerController.Yaw,
|
||||
playerVelocity: System.Numerics.Vector3.Zero,
|
||||
isOnGround: true,
|
||||
contactPlaneNormal: System.Numerics.Vector3.UnitZ,
|
||||
dt: 1f / 60f);
|
||||
|
||||
// 5. Return to InWorld.
|
||||
_playerController.State = AcDream.App.Input.PlayerState.InWorld;
|
||||
Console.WriteLine($"live: teleport complete — snapped to {snappedPos} cell=0x{resolved.CellId:X8}");
|
||||
|
||||
// 5. Send LoginComplete to tell the server the client finished loading.
|
||||
// Per holtburger's PlayerTeleport handler (client/messages.rs:434-440),
|
||||
// retail clients call send_login_complete() after each portal transition.
|
||||
// ResetLoginComplete() clears the latch so the 0xF746 PlayerCreate path
|
||||
// doesn't also send one. We send directly here instead.
|
||||
_liveSession?.SendGameAction(
|
||||
AcDream.Core.Net.Messages.GameActionLoginComplete.Build());
|
||||
// Recenter the streaming controller on the new landblock NOW (kick
|
||||
// off the dungeon load). After recentering, the destination is
|
||||
// (p.PositionX, p.PositionY, p.PositionZ) relative to the new origin.
|
||||
_liveCenterX = lbX;
|
||||
_liveCenterY = lbY;
|
||||
newWorldPos = new System.Numerics.Vector3(p.PositionX, p.PositionY, p.PositionZ);
|
||||
}
|
||||
else
|
||||
{
|
||||
newWorldPos = worldPos;
|
||||
}
|
||||
|
||||
// G.3a: do NOT snap here. The destination dungeon landblock has not
|
||||
// streamed in yet; an immediate Resolve falls back to the resident
|
||||
// (old) landblocks and lands the player in ocean (#133). HOLD the snap
|
||||
// in portal space — TeleportArrivalController.Tick (per frame) places
|
||||
// the player via PlaceTeleportArrival once the destination cell
|
||||
// hydrates (TeleportArrivalReadiness == Ready), or force-places on an
|
||||
// impossible claim / timeout. PortalSpace keeps input frozen meanwhile.
|
||||
EnsureTeleportArrivalController();
|
||||
_pendingTeleportRot = rot;
|
||||
_teleportArrival!.BeginArrival(newWorldPos, p.LandblockId);
|
||||
}
|
||||
}
|
||||
|
||||
// G.3a (#133): holds a teleport arrival in portal space until the destination
|
||||
// dungeon landblock/cell has hydrated, then places the player via the unchanged
|
||||
// validated-claim Resolve path. Lazily constructed on the first teleport (all
|
||||
// runtime deps are wired by then).
|
||||
private AcDream.App.World.TeleportArrivalController? _teleportArrival;
|
||||
private System.Numerics.Quaternion _pendingTeleportRot = System.Numerics.Quaternion.Identity;
|
||||
|
||||
private void EnsureTeleportArrivalController()
|
||||
{
|
||||
if (_teleportArrival is not null) return;
|
||||
_teleportArrival = new AcDream.App.World.TeleportArrivalController(
|
||||
readiness: TeleportArrivalReadiness,
|
||||
place: PlaceTeleportArrival);
|
||||
}
|
||||
|
||||
// Reuses the #107 login readiness triplet (GameWindow.cs:1010-1024), evaluated
|
||||
// against the teleport's (destPos, destCell): an impossible indoor claim short-
|
||||
// circuits to immediate placement; otherwise hold until terrain is sampled and,
|
||||
// for an indoor cell, the cell struct has hydrated.
|
||||
private AcDream.App.World.ArrivalReadiness TeleportArrivalReadiness(
|
||||
System.Numerics.Vector3 destPos, uint destCell)
|
||||
{
|
||||
if (IsSpawnClaimUnhydratable(destCell))
|
||||
return AcDream.App.World.ArrivalReadiness.Impossible;
|
||||
if (_physicsEngine.SampleTerrainZ(destPos.X, destPos.Y) is null)
|
||||
return AcDream.App.World.ArrivalReadiness.NotReady;
|
||||
bool indoor = (destCell & 0xFFFFu) >= 0x0100u;
|
||||
if (indoor && !_physicsEngine.IsSpawnCellReady(destCell))
|
||||
return AcDream.App.World.ArrivalReadiness.NotReady;
|
||||
return AcDream.App.World.ArrivalReadiness.Ready;
|
||||
}
|
||||
|
||||
// The deferred snap (the original OnLivePositionUpdated steps 2-5), now run only
|
||||
// once the destination is ready (or force-run on impossible/timeout, logged loud).
|
||||
private void PlaceTeleportArrival(
|
||||
System.Numerics.Vector3 destPos, uint destCell, bool forced)
|
||||
{
|
||||
var resolved = _physicsEngine.Resolve(
|
||||
destPos, destCell, System.Numerics.Vector3.Zero, _playerController!.StepUpHeight);
|
||||
var snappedPos = new System.Numerics.Vector3(
|
||||
resolved.Position.X, resolved.Position.Y, resolved.Position.Z);
|
||||
|
||||
if (forced)
|
||||
Console.WriteLine(
|
||||
$"live: teleport HOLD gave up (impossible/timeout) — force-snapping " +
|
||||
$"cell=0x{destCell:X8} pos={destPos} -> 0x{resolved.CellId:X8} {snappedPos}");
|
||||
|
||||
if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe))
|
||||
{
|
||||
pe.SetPosition(snappedPos);
|
||||
pe.ParentCellId = resolved.CellId;
|
||||
pe.Rotation = _pendingTeleportRot;
|
||||
}
|
||||
_playerController.SetPosition(snappedPos, resolved.CellId);
|
||||
|
||||
_chaseCamera?.Update(snappedPos, _playerController.Yaw);
|
||||
_retailChaseCamera?.Update(snappedPos, _playerController.Yaw,
|
||||
playerVelocity: System.Numerics.Vector3.Zero,
|
||||
isOnGround: true,
|
||||
contactPlaneNormal: System.Numerics.Vector3.UnitZ,
|
||||
dt: 1f / 60f);
|
||||
|
||||
_playerController.State = AcDream.App.Input.PlayerState.InWorld;
|
||||
Console.WriteLine($"live: teleport complete — snapped to {snappedPos} cell=0x{resolved.CellId:X8}");
|
||||
|
||||
// Tell the server the client finished loading the new landblock (holtburger
|
||||
// client/messages.rs:434 — re-send LoginComplete after each portal transition).
|
||||
_liveSession?.SendGameAction(
|
||||
AcDream.Core.Net.Messages.GameActionLoginComplete.Build());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase B.3: fires when the server sends a PlayerTeleport (0xF751).
|
||||
/// Freeze movement input by setting the player controller to PortalSpace.
|
||||
|
|
@ -4972,6 +5002,7 @@ public sealed class GameWindow : IDisposable
|
|||
{
|
||||
if (_playerController is not null)
|
||||
_playerController.State = AcDream.App.Input.PlayerState.PortalSpace;
|
||||
EnsureTeleportArrivalController();
|
||||
Console.WriteLine($"live: teleport started (seq={sequence})");
|
||||
}
|
||||
|
||||
|
|
@ -6837,6 +6868,12 @@ public sealed class GameWindow : IDisposable
|
|||
// Step 2: routed through the controller; functionally identical.
|
||||
_liveSessionController?.Tick();
|
||||
|
||||
// G.3a (#133): advance any held teleport arrival. Runs AFTER streaming
|
||||
// (which applies the destination landblock) and the live-session drain
|
||||
// (which may have just called BeginArrival), so a destination that
|
||||
// hydrated this frame is placed the same frame.
|
||||
_teleportArrival?.Tick();
|
||||
|
||||
// Phase K.1a — tick the input dispatcher so Hold-type bindings
|
||||
// re-fire while their chord is held. K.1b adds the subscribers
|
||||
// that actually consume the events.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue