fix(teleport): dungeon-exit priority-apply footgun + wall-clock timeout + arrival facing

T6 visual gate found three issues; probe pinned the roots.

- StreamingController.DrainAndApply: the priority hunt + outbox drain were gated
  behind 'if (budget <= 0) return;' AFTER the deferred-buffer drain. During a
  dungeon-exit expand (~600 completions) the buffer is always >= budget, so the
  hunt never ran and the destination never priority-applied (APPLY stalled ~5s;
  dest built +265ms but applied +6s). Restructured: priority hunt runs FIRST and
  unconditionally. Indoor (single-LB collapse) was unaffected, which is why only
  outdoor exits broke.
- Teleport timeout was a 600-FRAME count; the empty-world exit hold renders at
  ~1000fps so it fired in ~0.6s and force-placed into the skybox before the
  expand finished. Now wall-clock (10s) — worldReady wins the race.
- Arrival facing: synced _playerController.Yaw to the server orientation via the
  exact inverse of YawToAcQuaternion (ExtractYawFromQuaternion has a 270deg offset
  vs our yaw). Previously only the render mesh got the rotation → camera/movement
  faced the stale pre-teleport direction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-22 14:27:55 +02:00
parent 3ce1fae332
commit 6e78fcd7f6
2 changed files with 63 additions and 56 deletions

View file

@ -5473,7 +5473,7 @@ public sealed class GameWindow : IDisposable
_pendingTeleportRot = rot;
_pendingTeleportPos = newWorldPos;
_pendingTeleportCell = p.LandblockId;
_teleportHoldFrames = 0;
_teleportHoldSeconds = 0f;
_teleportForced = false;
if (_streamingController is not null)
_streamingController.PriorityLandblockId =
@ -5492,15 +5492,17 @@ public sealed class GameWindow : IDisposable
private bool _teleportInProgress;
private System.Numerics.Vector3 _pendingTeleportPos;
private uint _pendingTeleportCell;
private int _teleportHoldFrames; // frames waiting for residency (safety-net timeout)
private float _teleportHoldSeconds; // wall-clock seconds waiting for residency (safety-net timeout)
private bool _teleportForced; // true when the safety-net timeout force-places
private float _teleportFadeAlpha; // 0 = clear, 1 = full black; consumed by the fade overlay
private System.Numerics.Quaternion _pendingTeleportRot = System.Numerics.Quaternion.Identity;
// ~10s at 60fps. Loud safety net for a destination that never streams (worker crash /
// corrupt dat / OOB coords) — mirrors the retired controller's 600-frame ceiling. Now
// rarely fires because priority-apply makes residency fast.
private const int TeleportMaxHoldFrames = 600;
// Loud safety net for a destination that never streams (worker crash / corrupt dat /
// OOB coords). WALL-CLOCK, not a frame count: during a dungeon-exit hold the source is
// unloaded and the destination not yet loaded → empty world → ~1000 fps, so a 600-FRAME
// ceiling fired in ~0.6s and force-placed into the skybox before the expand finished.
// Now rarely fires because priority-apply makes residency fast.
private const float TeleportMaxHoldSeconds = 10f;
// #145: the LANDBLOCK-relative (cell-local) position used to SEED the player
// body's cell-relative CellPosition. This is the ONE place the streaming center
@ -5558,6 +5560,11 @@ public sealed class GameWindow : IDisposable
}
_playerController.SetPosition(snappedPos, resolved.CellId,
CellLocalForSeed(snappedPos, resolved.CellId));
// Face the server-specified destination heading (retail drops you facing a fixed
// direction). The render entity already got _pendingTeleportRot above; sync the
// controller yaw so the camera + movement frame match it instead of the stale
// pre-teleport facing.
_playerController.Yaw = AcQuaternionToYaw(_pendingTeleportRot);
_chaseCamera?.Update(snappedPos, _playerController.Yaw);
_retailChaseCamera?.Update(snappedPos, _playerController.Yaw,
@ -5586,9 +5593,9 @@ public sealed class GameWindow : IDisposable
{
if (_playerController is not null)
_playerController.State = AcDream.App.Input.PlayerState.PortalSpace;
_teleportInProgress = true;
_teleportHoldFrames = 0;
_teleportForced = false;
_teleportInProgress = true;
_teleportHoldSeconds = 0f;
_teleportForced = false;
_teleportAnim.Begin(AcDream.Core.World.TeleportEntryKind.Portal);
Console.WriteLine($"live: teleport started (seq={sequence})");
}
@ -7588,10 +7595,14 @@ public sealed class GameWindow : IDisposable
{
bool haveDest = _pendingTeleportCell != 0u;
bool ready = haveDest && TeleportWorldReady(_pendingTeleportCell);
if (haveDest && !ready && ++_teleportHoldFrames >= TeleportMaxHoldFrames)
if (haveDest && !ready)
{
ready = true;
_teleportForced = true;
_teleportHoldSeconds += (float)dt;
if (_teleportHoldSeconds >= TeleportMaxHoldSeconds)
{
ready = true;
_teleportForced = true;
}
}
var (snap, evts) = _teleportAnim.Tick((float)dt, ready);
@ -8077,6 +8088,22 @@ public sealed class GameWindow : IDisposable
1f - 2f * (q.Y * q.Y + q.Z * q.Z));
}
/// <summary>
/// Exact inverse of <see cref="YawToAcQuaternion"/>: AC wire orientation → our internal
/// yaw (radians, 0=+X East). NOT the same as <see cref="ExtractYawFromQuaternion"/>, which
/// returns the raw quaternion Z-angle (AC's <c>theta = 450 - heading</c>) — that carries a
/// 270° offset vs our yaw. Inverting the documented chain: theta = 450 - heading and
/// heading = 180 - yaw ⇒ yaw = thetaDeg - 270. Used to face the player the server-specified
/// way on a teleport arrival (retail drops you facing the portal's destination heading).
/// </summary>
private static float AcQuaternionToYaw(System.Numerics.Quaternion q)
{
float thetaDeg = ExtractYawFromQuaternion(q) * (180f / MathF.PI);
float yawDeg = thetaDeg - 270f; // 180 - (450 - thetaDeg)
float yaw = yawDeg * (MathF.PI / 180f);
return MathF.Atan2(MathF.Sin(yaw), MathF.Cos(yaw)); // normalize to (-PI, PI]
}
private void OnCameraModeChanged(bool _modeBool)
{
if (_input is null) return;