fix(physics): guard the world-frame agreement proven unreachable by measurement
Closes #283 (plan S3) - as UNREACHABLE, not by restructuring ownership. acdream has two owners that convert a landblock-local network origin into the streamed world frame: LiveWorldOriginState for presentation/streaming, and RuntimePhysicsState.TryGetWorldFrameOffset for placement. They rebase on different edges - Runtime the instant an accepted Position carries TeleportAdvanced, App only once StreamingOriginRecenterCoordinator observes old-window retirement completion, many frames later. A one-landblock disagreement places an entity 192 m from the geometry around it: the same failure family as the zero-offset bug670f307cfixed, with a wrong origin instead of a missing one. The plan's first step was to prove or disprove reachability BEFORE moving ownership, because a restructure on a hypothesis is churn. The probe added in898ff18banswered it: a connected Release session recorded ZERO disagreements across 11 completed reveals and six destination landblocks (0x0904, 0x1134, 0x3032, 0x8763, 0xA9B4, 0xF682) spanning roughly 45 km. A gap of even one frame would have printed an offset in the tens of thousands of metres. Cause of the safety: BeginOriginRecenter detaches EVERY resident landblock before the new origin is adopted, so the two rebases are serialized and no conversion can observe the gap. Ownership is therefore left exactly as it is. What lands instead is the invariant that keeps it true. LiveWorldOriginState.EnsureAgreesWithRuntimeFrame is checked at the landblock->world conversion and is terminal on disagreement, converting a silent 192 m-multiple misplacement into a loud failure with the offset in metres and the landblock being projected. Six focused tests pin it, including the cross-world portal case (0x09 -> 0xF6 = 45,504 m). Disagreement can no longer reach the probe, so ACDREAM_PROBE_WORLD_FRAME now emits a verbose per-conversion agreement trace - useful when a placement looks displaced for some reason OTHER than a frame disagreement. Complete Release solution: 10,844 passed / 4 skipped / 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
898ff18b26
commit
89cf1e66d0
5 changed files with 183 additions and 22 deletions
|
|
@ -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`)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/// <c>LiveWorldOriginState</c> 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
|
||||
/// (<c>LiveWorldOriginState.EnsureAgreesWithRuntimeFrame</c>, 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.
|
||||
/// </summary>
|
||||
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}"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,48 @@ internal sealed class LiveWorldOriginState
|
|||
|
||||
public (int X, int Y) GetCenter() => (CenterX, CenterY);
|
||||
|
||||
/// <summary>
|
||||
/// #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,
|
||||
/// <c>RuntimePhysicsState.TryGetWorldFrameOffset</c> for placement — so a
|
||||
/// disagreement of one landblock places an entity 192 m from the geometry
|
||||
/// around it.
|
||||
///
|
||||
/// <para>They rebase on different edges: Runtime the instant an accepted
|
||||
/// Position carries <c>TeleportAdvanced</c>, this owner only once
|
||||
/// <c>StreamingOriginRecenterCoordinator</c> observes old-window
|
||||
/// retirement completion. A 2026-08-03 connected run
|
||||
/// (<c>ACDREAM_PROBE_WORLD_FRAME=1</c>, 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.</para>
|
||||
/// </summary>
|
||||
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.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the streamed render frame back into retail's landblock-local
|
||||
/// physics frame at the one placement seed boundary.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
///
|
||||
/// <para>When true, every projected placement compares Runtime's frame
|
||||
/// centre against App's origin centre and emits one
|
||||
/// <c>[world-frame]</c> 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
|
||||
/// <para>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
|
||||
/// (<c>LiveWorldOriginState.EnsureAgreesWithRuntimeFrame</c>) rather than
|
||||
/// something to observe.</para>
|
||||
///
|
||||
/// <para>When true, this now emits one verbose <c>[world-frame] agree</c>
|
||||
/// 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
|
||||
/// <c>ACDREAM_PROBE_WORLD_FRAME=1</c>.</para>
|
||||
/// </summary>
|
||||
public static bool ProbeWorldFrameEnabled { get; set; } =
|
||||
|
|
|
|||
|
|
@ -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<InvalidOperationException>(() =>
|
||||
state.EnsureAgreesWithRuntimeFrame(
|
||||
runtimeCenter,
|
||||
0xA9B60001u));
|
||||
|
||||
Assert.Contains("World-frame owners disagree", error.Message);
|
||||
Assert.Contains(expectedOffset, error.Message);
|
||||
Assert.Contains("0xA9B60001", error.Message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ARuntimeRebaseAheadOfTheStreamedOrigin_IsCaught()
|
||||
{
|
||||
var state = new LiveWorldOriginState();
|
||||
Assert.True(state.TryInitialize(0x09, 0x04));
|
||||
|
||||
InvalidOperationException error =
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue