test(runtime): restore the world-frame precondition across first-entry fixtures

670f307c made remote first-entry placement resolve its landblock-local
CreateObject origin through Runtime's world frame
(RuntimeSetPositionState.PrepareMover:1526-1544) and return
RetrySetupUnavailable until that frame exists. Only the accepted local-player
Create publishes it (RuntimeEntityObjectLifetime.RegisterEntityCore:558-570 ->
RuntimePhysicsState.ObserveLocalWorldFrame).

Fixtures that drive remote conductors in a world with no local player - a
state production never occupies, since the player's own Create always precedes
broadcast Creates - therefore parked forever on RetrySetupUnavailable. Their
initial-create residences never retired, which cascaded into rejected
appearance updates, missing canonical bodies, unconverged ownership ledgers,
and a GameRuntime teardown that could not complete stage 10.

The measured blast radius was far larger than the handoff recorded. It claimed
"six selected fixture failures"; a baseline run found 43. The App suite was
fully green at 01f4791e and 670f307c broke 28 tests at once; the Runtime suite
lost 13, twelve of them in RuntimeRemoteFirstEntryStateTests - the exact
conductor that commit gated. Both commits were verified on focused runs only.

The production gate is correct, so nothing here weakens it. It matches App's
own coordinate owner: LiveWorldOriginState is initialized once from the local
player's spawn (LiveEntityHydrationPorts.cs:226) and rebased only by
StreamingOriginRecenterCoordinator.Advance at a teleport boundary - exactly
ObserveLocalWorldFrame's semantics. Every fixture is repaired by supplying the
missing precondition beside the resident landblock it already models, and not
one expected value or assertion was changed.

The mechanism shipped with zero tests. RuntimeWorldFrameTests now pins its
contract: the local player publishes the frame, remotes never do, neighbouring
landblocks convert at 192 m per step, ordinary movement across a landblock
boundary must NOT rebase it, an accepted teleport must, and a zero cell id
neither publishes nor resolves. That "no rebase on ordinary movement" rule is
load-bearing - if it and LiveWorldOriginState ever disagree, remote objects
are placed a multiple of 192 m from where the world is streamed.

Runtime 1,009/1,009; App 4,048 passed / 3 skipped.

Refs #281.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-03 13:32:28 +02:00
parent c7d5fc14b9
commit 6dcb94ac1b
6 changed files with 310 additions and 0 deletions

View file

@ -79,6 +79,22 @@ internal static class LiveEntityRuntimeFixture
worldOffsetY: 0f);
lifetime.Physics.SetPosition.CommitCollisionGeneration(
landblockId & 0xFFFF0000u, 1UL, ready: true);
// 670f307c: Create frames are landblock-local, so a first-entry
// conductor resolves them through Runtime's world frame
// (RuntimeSetPositionState.PrepareMover:1526-1544) and parks on
// RetrySetupUnavailable until that frame exists. Only the accepted
// local-player Create publishes it
// (RuntimeEntityObjectLifetime.RegisterEntityCore:558-570 ->
// RuntimePhysicsState.ObserveLocalWorldFrame), and production always
// registers the player before driving any broadcast Create. Model
// that ambient world fact beside the resident landblock above rather
// than registering a second entity, which would consume a local
// entity id and perturb every exact-ledger assertion built on this
// fixture. RuntimeWorldFrameTests covers the real ordering.
lifetime.Physics.ObserveLocalWorldFrame(
(landblockId & 0xFFFF0000u) | 0x0001u,
teleportAdvanced: false);
var runtime = new LiveEntityRuntime(spatial, resources, lifetime);
var movement = new AcDream.Runtime.Gameplay
.RuntimeLocalPlayerMovementState();
@ -111,6 +127,7 @@ internal static class LiveEntityRuntimeFixture
lifetime.Placements,
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
new AckOnlyPlacementSink(runtime));
return new DrivenLiveEntityRuntime
{
Runtime = runtime,

View file

@ -2091,6 +2091,19 @@ public sealed class LiveEntityHydrationControllerTests
worldOffsetY: 0f);
EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
Cell & 0xFFFF0000u, 1UL, ready: true);
// 670f307c: Create frames are landblock-local, so a first-entry
// conductor resolves them through Runtime's world frame
// (RuntimeSetPositionState.PrepareMover:1526-1544) and parks on
// RetrySetupUnavailable until it exists. Only the accepted
// local-player Create publishes it
// (RuntimeEntityObjectLifetime.RegisterEntityCore:558-570), and
// production always registers the player first. Model that
// ambient world fact beside the resident landblock above so
// these hydration tests keep their exact single-entity ledgers.
EntityObjects.Physics.ObserveLocalWorldFrame(
Cell,
teleportAdvanced: false);
Movement = new AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState();
IdentityState = new AcDream.Runtime.Gameplay.RuntimeLocalPlayerIdentityState();
var publication = new AcDream.Runtime.Gameplay

View file

@ -450,6 +450,22 @@ public sealed class RuntimeFirstEntryHostIntegrationTests
worldOffsetY: 0f);
EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
Cell & 0xFFFF0000u, 1UL, ready: true);
// 670f307c: a first-entry conductor resolves its landblock-local
// Create origin through Runtime's world frame
// (RuntimeSetPositionState.PrepareMover:1526-1544) and parks on
// RetrySetupUnavailable until it exists. Production publishes it
// from the accepted local-player Create
// (RuntimeEntityObjectLifetime.RegisterEntityCore:558-570). The
// playerGuid: 0 cases below deliberately model a REMOTE entity,
// so the frame must come from the ambient world here exactly as
// it does in LiveEntityRuntimeFixture — otherwise the graphical
// and direct hosts under comparison would differ only by a
// missing precondition. RuntimeWorldFrameTests covers the real
// local-player ordering.
EntityObjects.Physics.ObserveLocalWorldFrame(
Cell,
teleportAdvanced: false);
Movement = new AcDream.Runtime.Gameplay
.RuntimeLocalPlayerMovementState();
var runtimeIdentity = new AcDream.Runtime.Gameplay

View file

@ -923,6 +923,21 @@ public sealed class RuntimeRemoteFirstEntryStateTests
var generation = new RuntimeGenerationToken(1UL);
Lifetime.BindEventContext(() => generation, static () => 1UL);
// 670f307c: CreateObject frames are landblock-local, so a remote
// conductor's PrepareMover resolves them through Runtime's world
// frame (RuntimeSetPositionState.PrepareMover:1526-1544) and
// returns RetrySetupUnavailable until that frame exists. Only the
// accepted local-player Create publishes it
// (RuntimeEntityObjectLifetime.RegisterEntityCore:558-570 ->
// RuntimePhysicsState.ObserveLocalWorldFrame) — production always
// registers the player before any broadcast Create is driven.
// Model that ambient world fact here, alongside the resident
// terrain above, rather than registering a second entity that
// would perturb every ownership-ledger assertion in this class.
Lifetime.Physics.ObserveLocalWorldFrame(
Cell,
teleportAdvanced: false);
// The SAME conductor instance RuntimeEntityObjectLifetime itself
// constructs and wires into the residence's multicast retirement
// fan-out and BeginSessionClear — never a standalone copy — so

View file

@ -0,0 +1,236 @@
using System.Numerics;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
namespace AcDream.Runtime.Tests.Physics;
/// <summary>
/// Coverage for the world-frame owner introduced by <c>670f307c</c>
/// (<c>RuntimePhysicsState.ObserveLocalWorldFrame</c> /
/// <c>TryGetWorldFrameOffset</c>). CreateObject and Position frames are
/// landblock-local; Runtime converts them into the one streamed world frame
/// before <c>SetPosition</c>. That mechanism shipped with no tests, and its
/// absence silently parked every remote first-entry placement on
/// <c>RetrySetupUnavailable</c>, so these pin its exact contract:
/// the local player publishes it, remotes never do, ordinary movement across
/// a landblock boundary must NOT rebase it, and an accepted teleport must.
///
/// The "no rebase on ordinary movement" rule is not an arbitrary choice — it
/// matches App's own coordinate owner, <c>LiveWorldOriginState</c>, whose
/// <c>Recenter</c> is called only from
/// <c>StreamingOriginRecenterCoordinator.Advance</c> at a teleport boundary.
/// If these two ever disagree, remote objects are placed at a multiple of
/// 192 m from where the world is streamed.
/// </summary>
public sealed class RuntimeWorldFrameTests
{
private const uint CenterLandblock = 0xA9B60000u;
private const uint CenterCell = CenterLandblock | 0x0001u;
[Fact]
public void LocalPlayerCreate_PublishesTheWorldFrameAtItsLandblock()
{
using var lifetime = new RuntimeEntityObjectLifetime();
BindGeneration(lifetime);
Assert.False(lifetime.Physics.TryGetWorldFrameOffset(
CenterCell,
out _,
out _));
lifetime.RegisterEntityWithInitialResidence(
Spawn(0x50000001u, CenterCell),
isLocalPlayer: true);
Assert.True(lifetime.Physics.TryGetWorldFrameOffset(
CenterCell,
out float offsetX,
out float offsetY));
Assert.Equal(0f, offsetX);
Assert.Equal(0f, offsetY);
}
[Fact]
public void RemoteCreate_NeverPublishesTheWorldFrame()
{
using var lifetime = new RuntimeEntityObjectLifetime();
BindGeneration(lifetime);
lifetime.RegisterEntityWithInitialResidence(
Spawn(0x70000001u, CenterCell),
isLocalPlayer: false);
// Without the frame a remote conductor must park rather than commit a
// landblock-local origin as if it were already a world position.
Assert.False(lifetime.Physics.TryGetWorldFrameOffset(
CenterCell,
out _,
out _));
}
[Theory]
// One landblock east is +192 m on X; one north is +192 m on Y.
[InlineData(0xAAB60001u, 192f, 0f)]
[InlineData(0xA8B60001u, -192f, 0f)]
[InlineData(0xA9B70001u, 0f, 192f)]
[InlineData(0xA9B50001u, 0f, -192f)]
public void NeighbouringLandblocks_ConvertAt192MetresPerStep(
uint cellId,
float expectedX,
float expectedY)
{
using var lifetime = new RuntimeEntityObjectLifetime();
BindGeneration(lifetime);
lifetime.Physics.ObserveLocalWorldFrame(
CenterCell,
teleportAdvanced: false);
Assert.True(lifetime.Physics.TryGetWorldFrameOffset(
cellId,
out float offsetX,
out float offsetY));
Assert.Equal(expectedX, offsetX);
Assert.Equal(expectedY, offsetY);
}
[Fact]
public void OrdinaryMovementAcrossALandblockBoundary_DoesNotRebaseTheFrame()
{
using var lifetime = new RuntimeEntityObjectLifetime();
BindGeneration(lifetime);
lifetime.Physics.ObserveLocalWorldFrame(
CenterCell,
teleportAdvanced: false);
// Walking east into the next landblock: an accepted Position, but no
// teleport. The streamed world keeps its origin, so the frame must
// stay put and that landblock must still convert to +192 m.
lifetime.Physics.ObserveLocalWorldFrame(
0xAAB60001u,
teleportAdvanced: false);
Assert.True(lifetime.Physics.TryGetWorldFrameOffset(
0xAAB60001u,
out float offsetX,
out float offsetY));
Assert.Equal(192f, offsetX);
Assert.Equal(0f, offsetY);
}
[Fact]
public void AcceptedTeleport_RebasesTheFrameOnTheDestination()
{
using var lifetime = new RuntimeEntityObjectLifetime();
BindGeneration(lifetime);
lifetime.Physics.ObserveLocalWorldFrame(
CenterCell,
teleportAdvanced: false);
lifetime.Physics.ObserveLocalWorldFrame(
0xAAB60001u,
teleportAdvanced: true);
// The destination is now the origin, and the departure landblock sits
// one step west of it.
Assert.True(lifetime.Physics.TryGetWorldFrameOffset(
0xAAB60001u,
out float destinationX,
out float destinationY));
Assert.Equal(0f, destinationX);
Assert.Equal(0f, destinationY);
Assert.True(lifetime.Physics.TryGetWorldFrameOffset(
CenterCell,
out float sourceX,
out _));
Assert.Equal(-192f, sourceX);
}
[Fact]
public void AZeroCellIdNeitherPublishesNorResolves()
{
using var lifetime = new RuntimeEntityObjectLifetime();
BindGeneration(lifetime);
lifetime.Physics.ObserveLocalWorldFrame(0u, teleportAdvanced: false);
Assert.False(lifetime.Physics.TryGetWorldFrameOffset(
CenterCell,
out _,
out _));
lifetime.Physics.ObserveLocalWorldFrame(
CenterCell,
teleportAdvanced: false);
Assert.False(lifetime.Physics.TryGetWorldFrameOffset(
0u,
out _,
out _));
}
private static void BindGeneration(RuntimeEntityObjectLifetime lifetime)
{
var generation = new RuntimeGenerationToken(1UL);
lifetime.BindEventContext(() => generation, static () => 1UL);
}
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell)
{
var position = new CreateObject.ServerPosition(
cell, 1f, 2f, 3f, 1f, 0f, 0f, 0f);
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: 1);
var physics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.ReportCollisions,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: null,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: 1f,
Friction: 0.5f,
Elasticity: 0.05f,
Translucency: null,
Velocity: Vector3.Zero,
Acceleration: null,
AngularVelocity: Vector3.Zero,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
Guid: guid,
Position: position,
SetupTableId: null,
AnimPartChanges: Array.Empty<CreateObject.AnimPartChange>(),
TextureChanges: Array.Empty<CreateObject.TextureChange>(),
SubPalettes: Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: null,
ObjScale: 1f,
Name: "world-frame-fixture",
ItemType: null,
MotionState: null,
MotionTableId: 0x09000001u,
PhysicsState: physics.RawState,
ObjectDescriptionFlags: 0x8u,
Friction: 0.5f,
Elasticity: 0.05f,
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
}

View file

@ -400,6 +400,19 @@ public sealed class RuntimeLiveEntitySessionControllerTests
worldOffsetY: 0f);
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
landblockId, 1UL, ready: true);
// 670f307c: a remote conductor resolves its landblock-local Create
// origin through Runtime's world frame
// (RuntimeSetPositionState.PrepareMover:1526-1544) and parks on
// RetrySetupUnavailable until that frame exists. Production publishes
// it from the accepted local-player Create
// (RuntimeEntityObjectLifetime.RegisterEntityCore:558-570), which
// always precedes broadcast Creates. Model it with the resident
// landblock it belongs to, so these direct-sink tests keep asserting
// exact single-entity ledgers.
runtime.EntityObjects.Physics.ObserveLocalWorldFrame(
landblockId | 0x0001u,
teleportAdvanced: false);
}
private static RuntimeFirstEntryDriveController CreateDrive(