diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 329e4ad7..d238c75f 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -5470,6 +5470,12 @@ public sealed class GameWindow : IDisposable // player position can't drift the observer off the dungeon and re-expand. if (_streamingController is not null && IsSealedDungeonCell(p.LandblockId)) _streamingController.PreCollapseToDungeon(lbX, lbY); + else + // Outdoor teleport: the render origin (_liveCenter) just moved. Drop ALL + // resident terrain so overlapping blocks on a NEARBY jump re-bake at the + // new origin instead of rendering shifted — the confirmed "terrain in the + // sky" arcs (stale slots offset by exactly deltaLB×192). 2026-06-22. + _streamingController?.ForceReloadWindow(); } else { diff --git a/src/AcDream.App/Streaming/StreamingController.cs b/src/AcDream.App/Streaming/StreamingController.cs index 28203d55..79f1740a 100644 --- a/src/AcDream.App/Streaming/StreamingController.cs +++ b/src/AcDream.App/Streaming/StreamingController.cs @@ -344,6 +344,31 @@ public sealed class StreamingController _region = rebuilt; } + /// + /// 2026-06-22: an OUTDOOR teleport moved the render origin (_liveCenter). Every + /// resident terrain block was baked relative to the OLD origin, so any block that survives + /// an INCREMENTAL recenter — which happens on a NEARBY jump where the old and new windows + /// overlap, e.g. (170,168)→(169,180) — renders shifted by the jump distance: the confirmed + /// "terrain in the sky" arcs (the stale slots were all offset by exactly deltaLB×192). + /// SYNCHRONOUSLY drop every resident landblock (render slot + physics + state) so none + /// survives into the next frame stale, and no async unload can race a re-bake, then null + /// the region so the next re-bootstraps the WHOLE window fresh at + /// the new origin (the near ring is priority-applied behind the fade; the rest streams in). + /// A sealed-dungeon destination uses instead. + /// + public void ForceReloadWindow() + { + _collapsed = false; + // Snapshot — RemoveLandblock mutates the loaded set we're iterating. + var ids = new List(_state.LoadedLandblockIds); + foreach (var id in ids) + { + _state.RemoveLandblock(id); + _removeTerrain?.Invoke(id); // frees the render slot + physics + cell registries + } + _region = null; // NormalTick re-creates + bootstraps the full window next frame + } + /// /// Apply streamed completions for this frame. LOADS (terrain mesh GPU uploads) are the /// expensive part, so they are metered at to avoid a