diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 96abb8b3..8c1ac48e 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -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)); } + /// + /// Exact inverse of : AC wire orientation → our internal + /// yaw (radians, 0=+X East). NOT the same as , which + /// returns the raw quaternion Z-angle (AC's theta = 450 - heading) — 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). + /// + 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; diff --git a/src/AcDream.App/Streaming/StreamingController.cs b/src/AcDream.App/Streaming/StreamingController.cs index f5f9f43d..f2119b2e 100644 --- a/src/AcDream.App/Streaming/StreamingController.cs +++ b/src/AcDream.App/Streaming/StreamingController.cs @@ -332,66 +332,46 @@ public sealed class StreamingController /// private void DrainAndApply() { - int budget = MaxCompletionsPerFrame; - - // --- Step 1: drain the deferred buffer first (items held from a prior - // priority-hunt that overshot the cap). Apply up to `budget` of them. - int i1 = 0; - while (i1 < budget && i1 < _deferredApply.Count) { ApplyResult(_deferredApply[i1]); i1++; } - if (i1 > 0) _deferredApply.RemoveRange(0, i1); - budget -= i1; - - if (budget <= 0) return; - - // --- Step 2: priority hunt (only when a destination is set). + // --- Step 1: priority hunt FIRST and UNCONDITIONALLY (when a destination is set). + // The teleport destination must apply the frame the worker finishes building it, + // ahead of any deferred/outbox backlog — otherwise a big dungeon-exit expand + // (hundreds of completions) buries it and the arrival times out into the skybox. + // CRITICAL: this must NOT be gated behind the per-frame budget. The prior version + // ran the deferred drain first and returned when the budget hit 0, so once + // _deferredApply held >= MaxCompletionsPerFrame items the hunt (and the outbox + // drain) were skipped entirely — the destination never priority-applied. The hunt + // drains the outbox in bounded chunks; the priority is applied on match, every + // non-priority item is buffered in _deferredApply (no loss). if (PriorityLandblockId != 0u) { - // Drain in chunks of MaxCompletionsPerFrame (bounded at 64 total - // iterations to avoid an unbounded loop if the outbox is huge). - const int MaxHuntIterations = 64; + const int MaxHuntIterations = 64; // cap at 64 * MaxCompletionsPerFrame drained/frame int hunted = 0; - bool found = false; - - while (hunted < MaxHuntIterations && !found) + while (hunted < MaxHuntIterations) { var chunk = _drainCompletions(MaxCompletionsPerFrame); if (chunk.Count == 0) break; - foreach (var result in chunk) { hunted++; if (ResultLandblockId(result) == PriorityLandblockId) - { - 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. - } + ApplyResult(result); // applied immediately, ahead of the budget else - { _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 - // worker's outbox (priority was 0, or not found this frame). + // --- Step 2: apply up to MaxCompletionsPerFrame this frame — the deferred backlog + // 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); foreach (var result in drained) ApplyResult(result);