diff --git a/docs/ISSUES.md b/docs/ISSUES.md
index dad65f69..8f0951af 100644
--- a/docs/ISSUES.md
+++ b/docs/ISSUES.md
@@ -72,7 +72,7 @@ reconciling #281's 43 test failures.
lit statics are unchanged. The session's log corroborates it — 9 completed
world reveals, 58 reveal events all `failures=0`, zero unhandled exceptions,
and a graceful exit.
-- **#283 — OPEN — Runtime's world frame and App's render origin rebase at
+- **#283 — DONE (2026-08-03, proven unreachable) — Runtime's world frame and App's render origin rebase at
different moments during a teleport.** `670f307c` gave Runtime its own world
frame (`RuntimePhysicsState.ObserveLocalWorldFrame`), which rebases the
instant an accepted Position carries `TeleportAdvanced`. App's
@@ -87,6 +87,25 @@ reconciling #281's 43 test failures.
gate may or may not exclude Create during that window, and #280 says other
work does continue arriving through it. Two owners of one fact; the campaign
answer is that Runtime owns the frame and App projects it. Route-3 adjacent.
+ **Resolved 2026-08-03 as UNREACHABLE, not restructured.** The plan's first
+ step was to prove or disprove reachability before moving ownership.
+ `ACDREAM_PROBE_WORLD_FRAME=1` (`PhysicsDiagnostics.ProbeWorldFrameEnabled`)
+ compared both owners at
+ `DatLiveEntityProjectionMaterializer`'s landblock→world conversion — the
+ App-side counterpart of `TryGetWorldFrameOffset`. A connected Release
+ session recorded **zero** disagreements across 11 completed reveals and six
+ destination landblocks (`0x0904`, `0x1134`, `0x3032`, `0x8763`, `0xA9B4`,
+ `0xF682`) spanning ~45 km — a gap of even one frame would have printed an
+ offset in the tens of thousands of metres. Cause: `BeginOriginRecenter`
+ detaches EVERY resident landblock before the new origin is adopted, which
+ serializes the two rebases so no conversion can observe the gap.
+ Ownership is therefore left alone (restructuring on a disproven hypothesis
+ would have been churn). Instead
+ `LiveWorldOriginState.EnsureAgreesWithRuntimeFrame` is a permanent terminal
+ invariant at that conversion, turning a silent 192 m-multiple misplacement
+ into a loud failure if a future change ever reopens the window; six focused
+ tests pin it, including the cross-world portal case. The probe flag now
+ emits a verbose per-conversion agreement trace for future investigation.
- **#284 — DONE (2026-08-03) — a placement that cannot resolve parks forever with no
diagnostic.** A first-entry placement whose world frame is absent returns
`RetrySetupUnavailable` (`RuntimeSetPositionState.PrepareMover:1535-1543`)
diff --git a/src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs b/src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs
index e5f1abe3..8887903f 100644
--- a/src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs
+++ b/src/AcDream.App/Rendering/DatLiveEntityProjectionMaterializer.cs
@@ -163,6 +163,12 @@ internal sealed class DatLiveEntityProjectionMaterializer
CreateObject.ServerPosition position = canonicalSpawn.Position.Value;
int lbX = (int)((position.LandblockId >> 24) & 0xFFu);
int lbY = (int)((position.LandblockId >> 16) & 0xFFu);
+ // #283: permanent invariant - the two world-frame owners must agree
+ // before their conversions can be mixed. Proven unreachable by the
+ // 2026-08-03 probe run; this keeps it that way.
+ _origin.EnsureAgreesWithRuntimeFrame(
+ _runtime.Physics.WorldFrameCenterLandblockId,
+ position.LandblockId);
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeWorldFrameEnabled)
ProbeWorldFrameAgreement(position.LandblockId);
var worldOrigin = new Vector3(
@@ -1195,12 +1201,12 @@ internal sealed class DatLiveEntityProjectionMaterializer
/// must use the SAME centre landblock or the entity lands a multiple of
/// 192 m from the geometry around it.
///
- /// Runtime rebases on the accepted teleport Position; App's
- /// LiveWorldOriginState rebases only after old-window retirement
- /// completes. Emits one line per DISAGREEMENT — silence across a portal
- /// run is the evidence that the window is unreachable in practice, and
- /// that #283 can be closed as a permanent invariant rather than an
- /// ownership restructure. Measurement only; it never gates placement.
+ /// Disagreement is now a terminal invariant
+ /// (LiveWorldOriginState.EnsureAgreesWithRuntimeFrame, checked
+ /// immediately before this), so anything reaching here has already
+ /// agreed. This verbose trace records the centres both owners used for
+ /// each conversion, which is what you want when investigating a
+ /// placement that looks displaced but is not a frame disagreement.
///
private void ProbeWorldFrameAgreement(uint landblockId)
{
@@ -1210,15 +1216,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
int runtimeCenterX = (int)((runtimeCenter >> 24) & 0xFFu);
int runtimeCenterY = (int)((runtimeCenter >> 16) & 0xFFu);
- if (runtimeCenterX == _origin.CenterX
- && runtimeCenterY == _origin.CenterY)
- {
- return;
- }
-
- float deltaX = (runtimeCenterX - _origin.CenterX) * 192f;
- float deltaY = (runtimeCenterY - _origin.CenterY) * 192f;
Console.WriteLine(System.FormattableString.Invariant(
- $"[world-frame] DISAGREE runtime=({runtimeCenterX},{runtimeCenterY}) app=({_origin.CenterX},{_origin.CenterY}) offsetDelta=({deltaX:F0}m,{deltaY:F0}m) projecting=0x{landblockId:X8}"));
+ $"[world-frame] agree centre=({runtimeCenterX},{runtimeCenterY}) projecting=0x{landblockId:X8}"));
}
}
diff --git a/src/AcDream.App/World/LiveWorldOriginState.cs b/src/AcDream.App/World/LiveWorldOriginState.cs
index 551bb45c..e74290e7 100644
--- a/src/AcDream.App/World/LiveWorldOriginState.cs
+++ b/src/AcDream.App/World/LiveWorldOriginState.cs
@@ -44,6 +44,48 @@ internal sealed class LiveWorldOriginState
public (int X, int Y) GetCenter() => (CenterX, CenterY);
+ ///
+ /// #283: asserts that this owner and Runtime's world frame agree on which
+ /// landblock is (0,0). Both convert landblock-local network origins into
+ /// the streamed world frame — this one for presentation and streaming,
+ /// RuntimePhysicsState.TryGetWorldFrameOffset for placement — so a
+ /// disagreement of one landblock places an entity 192 m from the geometry
+ /// around it.
+ ///
+ /// They rebase on different edges: Runtime the instant an accepted
+ /// Position carries TeleportAdvanced, this owner only once
+ /// StreamingOriginRecenterCoordinator observes old-window
+ /// retirement completion. A 2026-08-03 connected run
+ /// (ACDREAM_PROBE_WORLD_FRAME=1, 11 reveals across six landblocks
+ /// spanning ~45 km) recorded ZERO disagreements: detaching every resident
+ /// landblock before adopting the new origin serializes the two rebases,
+ /// so no conversion can observe the gap. This is the permanent guard that
+ /// keeps that true — it converts a silent 192 m-multiple misplacement into
+ /// a loud failure if a future change ever reopens the window.
+ ///
+ public void EnsureAgreesWithRuntimeFrame(
+ uint runtimeCenterLandblockId,
+ uint projectingLandblockId)
+ {
+ // Before either owner is established there is nothing to agree on;
+ // the placement itself is gated separately (#284).
+ if (!IsKnown || runtimeCenterLandblockId == 0u)
+ return;
+
+ int runtimeCenterX = (int)((runtimeCenterLandblockId >> 24) & 0xFFu);
+ int runtimeCenterY = (int)((runtimeCenterLandblockId >> 16) & 0xFFu);
+ if (runtimeCenterX == CenterX && runtimeCenterY == CenterY)
+ return;
+
+ throw new InvalidOperationException(
+ "World-frame owners disagree: Runtime centre "
+ + $"({runtimeCenterX},{runtimeCenterY}) vs streamed origin "
+ + $"({CenterX},{CenterY}) while projecting landblock "
+ + $"0x{projectingLandblockId:X8}. That offsets the entity by "
+ + $"({(runtimeCenterX - CenterX) * 192f:F0}m,"
+ + $"{(runtimeCenterY - CenterY) * 192f:F0}m) from its geometry.");
+ }
+
///
/// Converts the streamed render frame back into retail's landblock-local
/// physics frame at the one placement seed boundary.
diff --git a/src/AcDream.Core/Physics/PhysicsDiagnostics.cs b/src/AcDream.Core/Physics/PhysicsDiagnostics.cs
index c42d4ebf..950f2c8c 100644
--- a/src/AcDream.Core/Physics/PhysicsDiagnostics.cs
+++ b/src/AcDream.Core/Physics/PhysicsDiagnostics.cs
@@ -71,12 +71,19 @@ public static class PhysicsDiagnostics
/// anything converted in that gap lands a multiple of 192 m from the
/// geometry App is building.
///
- /// When true, every projected placement compares Runtime's frame
- /// centre against App's origin centre and emits one
- /// [world-frame] line per DISAGREEMENT only — silence means the
- /// window is never observed. This exists to prove or disprove
- /// reachability before any ownership is restructured; it is a
- /// measurement, never a gate. Initial state from
+ /// A 2026-08-03 connected run answered that question: 11 reveals
+ /// across six landblocks spanning ~45 km recorded ZERO disagreements,
+ /// because the recenter detaches every resident landblock before adopting
+ /// the new origin and so serializes the two rebases. Disagreement is now
+ /// a terminal invariant
+ /// (LiveWorldOriginState.EnsureAgreesWithRuntimeFrame) rather than
+ /// something to observe.
+ ///
+ /// When true, this now emits one verbose [world-frame] agree
+ /// line per projected conversion recording the centre both owners used —
+ /// what you want when investigating a placement that looks displaced but
+ /// is NOT a frame disagreement. Measurement only; it never gates
+ /// placement. Initial state from
/// ACDREAM_PROBE_WORLD_FRAME=1.
///
public static bool ProbeWorldFrameEnabled { get; set; } =
diff --git a/tests/AcDream.App.Tests/World/LiveWorldOriginStateTests.cs b/tests/AcDream.App.Tests/World/LiveWorldOriginStateTests.cs
index b2298e78..722c0a36 100644
--- a/tests/AcDream.App.Tests/World/LiveWorldOriginStateTests.cs
+++ b/tests/AcDream.App.Tests/World/LiveWorldOriginStateTests.cs
@@ -73,4 +73,99 @@ public sealed class LiveWorldOriginStateTests
Assert.Equal(new Vector3(8f, 84f, 5f), local);
}
+
+ // #283: acdream has TWO owners that convert a landblock-local network
+ // origin into the streamed world frame - this one for presentation and
+ // streaming, RuntimePhysicsState.TryGetWorldFrameOffset for placement.
+ // They rebase on different edges (Runtime on the accepted teleport
+ // Position; this owner only once StreamingOriginRecenterCoordinator
+ // observes old-window retirement), so a one-landblock disagreement places
+ // an entity 192 m from the geometry around it - the same failure family
+ // as the zero-offset bug 670f307c fixed, with a wrong origin rather than
+ // a missing one.
+ //
+ // A 2026-08-03 connected run with ACDREAM_PROBE_WORLD_FRAME=1 (11 reveals
+ // across six landblocks spanning ~45 km) recorded ZERO disagreements:
+ // detaching every resident landblock before adopting the new origin
+ // serializes the two rebases. These pin the permanent guard.
+
+ [Fact]
+ public void AgreeingWorldFrameOwners_Pass()
+ {
+ var state = new LiveWorldOriginState();
+ Assert.True(state.TryInitialize(0xA9, 0xB6));
+
+ state.EnsureAgreesWithRuntimeFrame(0xA9B6FFFFu, 0xA9B60001u);
+ }
+
+ [Fact]
+ public void BeforeEitherWorldFrameOwnerIsEstablished_ThereIsNothingToAgreeOn()
+ {
+ var state = new LiveWorldOriginState();
+
+ // No accepted origin yet; placement is gated separately (#284).
+ state.EnsureAgreesWithRuntimeFrame(0xA9B6FFFFu, 0xA9B60001u);
+
+ // An accepted origin with no Runtime frame yet is equally not a
+ // disagreement.
+ Assert.True(state.TryInitialize(0xA9, 0xB6));
+ state.EnsureAgreesWithRuntimeFrame(0u, 0xA9B60001u);
+ }
+
+ [Theory]
+ [InlineData(0xAAB6FFFFu, "192m")]
+ [InlineData(0xA8B6FFFFu, "-192m")]
+ [InlineData(0xA9B7FFFFu, "192m")]
+ [InlineData(0xA9B5FFFFu, "-192m")]
+ public void DisagreeingWorldFrameOwners_FailLoudlyWithTheOffsetInMetres(
+ uint runtimeCenter,
+ string expectedOffset)
+ {
+ var state = new LiveWorldOriginState();
+ Assert.True(state.TryInitialize(0xA9, 0xB6));
+
+ InvalidOperationException error =
+ Assert.Throws(() =>
+ state.EnsureAgreesWithRuntimeFrame(
+ runtimeCenter,
+ 0xA9B60001u));
+
+ Assert.Contains("World-frame owners disagree", error.Message);
+ Assert.Contains(expectedOffset, error.Message);
+ Assert.Contains("0xA9B60001", error.Message);
+ }
+
+ ///
+ /// The exact case the guard exists for: Runtime has rebased on a portal
+ /// destination while the streamed origin still holds the departure
+ /// landblock. A cross-world portal makes the offset enormous rather than
+ /// subtle - 0xF6 - 0x09 = 237 landblocks east = 45,504 m.
+ ///
+ [Fact]
+ public void ARuntimeRebaseAheadOfTheStreamedOrigin_IsCaught()
+ {
+ var state = new LiveWorldOriginState();
+ Assert.True(state.TryInitialize(0x09, 0x04));
+
+ InvalidOperationException error =
+ Assert.Throws(() =>
+ state.EnsureAgreesWithRuntimeFrame(
+ 0xF682FFFFu,
+ 0xF6820033u));
+
+ Assert.Contains("45504m", error.Message);
+ }
+
+ [Fact]
+ public void AfterBothRebaseToTheSameDestination_TheyAgreeAgain()
+ {
+ var state = new LiveWorldOriginState();
+ Assert.True(state.TryInitialize(0x09, 0x04));
+
+ // The streamed origin adopts the destination once old-window
+ // retirement completes; Runtime is already there.
+ state.Recenter(0xF6, 0x82);
+
+ state.EnsureAgreesWithRuntimeFrame(0xF682FFFFu, 0xF6820033u);
+ }
}