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:
parent
3ce1fae332
commit
6e78fcd7f6
2 changed files with 63 additions and 56 deletions
|
|
@ -5473,7 +5473,7 @@ public sealed class GameWindow : IDisposable
|
||||||
_pendingTeleportRot = rot;
|
_pendingTeleportRot = rot;
|
||||||
_pendingTeleportPos = newWorldPos;
|
_pendingTeleportPos = newWorldPos;
|
||||||
_pendingTeleportCell = p.LandblockId;
|
_pendingTeleportCell = p.LandblockId;
|
||||||
_teleportHoldFrames = 0;
|
_teleportHoldSeconds = 0f;
|
||||||
_teleportForced = false;
|
_teleportForced = false;
|
||||||
if (_streamingController is not null)
|
if (_streamingController is not null)
|
||||||
_streamingController.PriorityLandblockId =
|
_streamingController.PriorityLandblockId =
|
||||||
|
|
@ -5492,15 +5492,17 @@ public sealed class GameWindow : IDisposable
|
||||||
private bool _teleportInProgress;
|
private bool _teleportInProgress;
|
||||||
private System.Numerics.Vector3 _pendingTeleportPos;
|
private System.Numerics.Vector3 _pendingTeleportPos;
|
||||||
private uint _pendingTeleportCell;
|
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 bool _teleportForced; // true when the safety-net timeout force-places
|
||||||
private float _teleportFadeAlpha; // 0 = clear, 1 = full black; consumed by the fade overlay
|
private float _teleportFadeAlpha; // 0 = clear, 1 = full black; consumed by the fade overlay
|
||||||
private System.Numerics.Quaternion _pendingTeleportRot = System.Numerics.Quaternion.Identity;
|
private System.Numerics.Quaternion _pendingTeleportRot = System.Numerics.Quaternion.Identity;
|
||||||
|
|
||||||
// ~10s at 60fps. Loud safety net for a destination that never streams (worker crash /
|
// Loud safety net for a destination that never streams (worker crash / corrupt dat /
|
||||||
// corrupt dat / OOB coords) — mirrors the retired controller's 600-frame ceiling. Now
|
// OOB coords). WALL-CLOCK, not a frame count: during a dungeon-exit hold the source is
|
||||||
// rarely fires because priority-apply makes residency fast.
|
// unloaded and the destination not yet loaded → empty world → ~1000 fps, so a 600-FRAME
|
||||||
private const int TeleportMaxHoldFrames = 600;
|
// 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
|
// #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
|
// 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,
|
_playerController.SetPosition(snappedPos, resolved.CellId,
|
||||||
CellLocalForSeed(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);
|
_chaseCamera?.Update(snappedPos, _playerController.Yaw);
|
||||||
_retailChaseCamera?.Update(snappedPos, _playerController.Yaw,
|
_retailChaseCamera?.Update(snappedPos, _playerController.Yaw,
|
||||||
|
|
@ -5586,9 +5593,9 @@ public sealed class GameWindow : IDisposable
|
||||||
{
|
{
|
||||||
if (_playerController is not null)
|
if (_playerController is not null)
|
||||||
_playerController.State = AcDream.App.Input.PlayerState.PortalSpace;
|
_playerController.State = AcDream.App.Input.PlayerState.PortalSpace;
|
||||||
_teleportInProgress = true;
|
_teleportInProgress = true;
|
||||||
_teleportHoldFrames = 0;
|
_teleportHoldSeconds = 0f;
|
||||||
_teleportForced = false;
|
_teleportForced = false;
|
||||||
_teleportAnim.Begin(AcDream.Core.World.TeleportEntryKind.Portal);
|
_teleportAnim.Begin(AcDream.Core.World.TeleportEntryKind.Portal);
|
||||||
Console.WriteLine($"live: teleport started (seq={sequence})");
|
Console.WriteLine($"live: teleport started (seq={sequence})");
|
||||||
}
|
}
|
||||||
|
|
@ -7588,10 +7595,14 @@ public sealed class GameWindow : IDisposable
|
||||||
{
|
{
|
||||||
bool haveDest = _pendingTeleportCell != 0u;
|
bool haveDest = _pendingTeleportCell != 0u;
|
||||||
bool ready = haveDest && TeleportWorldReady(_pendingTeleportCell);
|
bool ready = haveDest && TeleportWorldReady(_pendingTeleportCell);
|
||||||
if (haveDest && !ready && ++_teleportHoldFrames >= TeleportMaxHoldFrames)
|
if (haveDest && !ready)
|
||||||
{
|
{
|
||||||
ready = true;
|
_teleportHoldSeconds += (float)dt;
|
||||||
_teleportForced = true;
|
if (_teleportHoldSeconds >= TeleportMaxHoldSeconds)
|
||||||
|
{
|
||||||
|
ready = true;
|
||||||
|
_teleportForced = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (snap, evts) = _teleportAnim.Tick((float)dt, ready);
|
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));
|
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)
|
private void OnCameraModeChanged(bool _modeBool)
|
||||||
{
|
{
|
||||||
if (_input is null) return;
|
if (_input is null) return;
|
||||||
|
|
|
||||||
|
|
@ -332,66 +332,46 @@ public sealed class StreamingController
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void DrainAndApply()
|
private void DrainAndApply()
|
||||||
{
|
{
|
||||||
int budget = MaxCompletionsPerFrame;
|
// --- Step 1: priority hunt FIRST and UNCONDITIONALLY (when a destination is set).
|
||||||
|
// The teleport destination must apply the frame the worker finishes building it,
|
||||||
// --- Step 1: drain the deferred buffer first (items held from a prior
|
// ahead of any deferred/outbox backlog — otherwise a big dungeon-exit expand
|
||||||
// priority-hunt that overshot the cap). Apply up to `budget` of them.
|
// (hundreds of completions) buries it and the arrival times out into the skybox.
|
||||||
int i1 = 0;
|
// CRITICAL: this must NOT be gated behind the per-frame budget. The prior version
|
||||||
while (i1 < budget && i1 < _deferredApply.Count) { ApplyResult(_deferredApply[i1]); i1++; }
|
// ran the deferred drain first and returned when the budget hit 0, so once
|
||||||
if (i1 > 0) _deferredApply.RemoveRange(0, i1);
|
// _deferredApply held >= MaxCompletionsPerFrame items the hunt (and the outbox
|
||||||
budget -= i1;
|
// drain) were skipped entirely — the destination never priority-applied. The hunt
|
||||||
|
// drains the outbox in bounded chunks; the priority is applied on match, every
|
||||||
if (budget <= 0) return;
|
// non-priority item is buffered in _deferredApply (no loss).
|
||||||
|
|
||||||
// --- Step 2: priority hunt (only when a destination is set).
|
|
||||||
if (PriorityLandblockId != 0u)
|
if (PriorityLandblockId != 0u)
|
||||||
{
|
{
|
||||||
// Drain in chunks of MaxCompletionsPerFrame (bounded at 64 total
|
const int MaxHuntIterations = 64; // cap at 64 * MaxCompletionsPerFrame drained/frame
|
||||||
// iterations to avoid an unbounded loop if the outbox is huge).
|
|
||||||
const int MaxHuntIterations = 64;
|
|
||||||
int hunted = 0;
|
int hunted = 0;
|
||||||
bool found = false;
|
while (hunted < MaxHuntIterations)
|
||||||
|
|
||||||
while (hunted < MaxHuntIterations && !found)
|
|
||||||
{
|
{
|
||||||
var chunk = _drainCompletions(MaxCompletionsPerFrame);
|
var chunk = _drainCompletions(MaxCompletionsPerFrame);
|
||||||
if (chunk.Count == 0) break;
|
if (chunk.Count == 0) break;
|
||||||
|
|
||||||
foreach (var result in chunk)
|
foreach (var result in chunk)
|
||||||
{
|
{
|
||||||
hunted++;
|
hunted++;
|
||||||
if (ResultLandblockId(result) == PriorityLandblockId)
|
if (ResultLandblockId(result) == PriorityLandblockId)
|
||||||
{
|
ApplyResult(result); // applied immediately, ahead of the budget
|
||||||
ApplyResult(result);
|
|
||||||
budget--; // priority counts against the budget
|
|
||||||
found = true;
|
|
||||||
// Remaining items in this chunk go to deferred — they
|
|
||||||
// were drained past the cap to find the priority; don't
|
|
||||||
// drop them.
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
|
||||||
_deferredApply.Add(result);
|
_deferredApply.Add(result);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found)
|
|
||||||
{
|
|
||||||
// Now apply up to remaining budget from deferred (mix from the
|
|
||||||
// chunk we just buffered + any pre-existing deferred items).
|
|
||||||
int i2 = 0;
|
|
||||||
while (i2 < budget && i2 < _deferredApply.Count) { ApplyResult(_deferredApply[i2]); i2++; }
|
|
||||||
if (i2 > 0) _deferredApply.RemoveRange(0, i2);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Priority not found in the outbox this frame: fall through to
|
|
||||||
// normal drain (the deferred buffer was already partially applied
|
|
||||||
// above; budget was already decremented).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Step 3: normal path — drain up to remaining budget from the
|
// --- Step 2: apply up to MaxCompletionsPerFrame this frame — the deferred backlog
|
||||||
// worker's outbox (priority was 0, or not found this frame).
|
// first (FIFO), then fresh outbox completions. Caps GPU upload per frame so a big
|
||||||
|
// diff doesn't spike. While a priority is set, Step 1 has already moved the outbox
|
||||||
|
// into _deferredApply, so this drains that backlog at the budget rate.
|
||||||
|
int budget = MaxCompletionsPerFrame;
|
||||||
|
int i = 0;
|
||||||
|
while (i < budget && i < _deferredApply.Count) { ApplyResult(_deferredApply[i]); i++; }
|
||||||
|
if (i > 0) _deferredApply.RemoveRange(0, i);
|
||||||
|
budget -= i;
|
||||||
|
if (budget <= 0) return;
|
||||||
|
|
||||||
var drained = _drainCompletions(budget);
|
var drained = _drainCompletions(budget);
|
||||||
foreach (var result in drained)
|
foreach (var result in drained)
|
||||||
ApplyResult(result);
|
ApplyResult(result);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue