diff --git a/tests/AcDream.App.Tests/LiveEntityRuntimeFixture.cs b/tests/AcDream.App.Tests/LiveEntityRuntimeFixture.cs index 635459f7..d062412c 100644 --- a/tests/AcDream.App.Tests/LiveEntityRuntimeFixture.cs +++ b/tests/AcDream.App.Tests/LiveEntityRuntimeFixture.cs @@ -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, diff --git a/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs b/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs index 7b51ef7d..a0f5c329 100644 --- a/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs +++ b/tests/AcDream.App.Tests/World/LiveEntityHydrationControllerTests.cs @@ -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 diff --git a/tests/AcDream.App.Tests/World/RuntimeFirstEntryHostIntegrationTests.cs b/tests/AcDream.App.Tests/World/RuntimeFirstEntryHostIntegrationTests.cs index 829a1c25..a5834879 100644 --- a/tests/AcDream.App.Tests/World/RuntimeFirstEntryHostIntegrationTests.cs +++ b/tests/AcDream.App.Tests/World/RuntimeFirstEntryHostIntegrationTests.cs @@ -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 diff --git a/tests/AcDream.Runtime.Tests/Entities/RuntimeRemoteFirstEntryStateTests.cs b/tests/AcDream.Runtime.Tests/Entities/RuntimeRemoteFirstEntryStateTests.cs index 7f54988d..abf19a47 100644 --- a/tests/AcDream.Runtime.Tests/Entities/RuntimeRemoteFirstEntryStateTests.cs +++ b/tests/AcDream.Runtime.Tests/Entities/RuntimeRemoteFirstEntryStateTests.cs @@ -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 diff --git a/tests/AcDream.Runtime.Tests/Physics/RuntimeWorldFrameTests.cs b/tests/AcDream.Runtime.Tests/Physics/RuntimeWorldFrameTests.cs new file mode 100644 index 00000000..2f0f35f3 --- /dev/null +++ b/tests/AcDream.Runtime.Tests/Physics/RuntimeWorldFrameTests.cs @@ -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; + +/// +/// Coverage for the world-frame owner introduced by 670f307c +/// (RuntimePhysicsState.ObserveLocalWorldFrame / +/// TryGetWorldFrameOffset). CreateObject and Position frames are +/// landblock-local; Runtime converts them into the one streamed world frame +/// before SetPosition. That mechanism shipped with no tests, and its +/// absence silently parked every remote first-entry placement on +/// RetrySetupUnavailable, 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, LiveWorldOriginState, whose +/// Recenter is called only from +/// StreamingOriginRecenterCoordinator.Advance 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. +/// +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(), + TextureChanges: Array.Empty(), + SubPalettes: Array.Empty(), + 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); + } +} diff --git a/tests/AcDream.Runtime.Tests/Session/RuntimeLiveEntitySessionControllerTests.cs b/tests/AcDream.Runtime.Tests/Session/RuntimeLiveEntitySessionControllerTests.cs index 123e68ac..d30b6a9e 100644 --- a/tests/AcDream.Runtime.Tests/Session/RuntimeLiveEntitySessionControllerTests.cs +++ b/tests/AcDream.Runtime.Tests/Session/RuntimeLiveEntitySessionControllerTests.cs @@ -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(