using System.Numerics;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Physics;
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);
}
///
/// #284: waiting for the world frame is legitimate only while the
/// local-player Create is still outstanding. The frame is published once
/// per session from that Create, so once it has been accepted WITHOUT
/// publishing one - a local-player CreateObject carrying no landblock -
/// no later pump can ever supply it, and every remote placement would
/// retry forever in silence. That is a violated invariant, not a wait.
///
[Fact]
public void ALocalPlayerCreateWithNoLandblock_MakesTheFrameUnreachable()
{
using var lifetime = new RuntimeEntityObjectLifetime();
BindGeneration(lifetime);
// Before the local player is seen at all, waiting is legitimate.
lifetime.Physics.ThrowIfWorldFrameUnreachable(CenterCell);
lifetime.Physics.ObserveLocalPlayerCreate(0u);
InvalidOperationException error =
Assert.Throws(() =>
lifetime.Physics.ThrowIfWorldFrameUnreachable(CenterCell));
Assert.Contains("world frame is unreachable", error.Message);
}
[Fact]
public void AnAcceptedLocalPlayerCreate_LeavesTheFrameReachable()
{
using var lifetime = new RuntimeEntityObjectLifetime();
BindGeneration(lifetime);
lifetime.Physics.ObserveLocalPlayerCreate(CenterCell);
// The frame exists, so nothing is unreachable and nothing throws.
lifetime.Physics.ThrowIfWorldFrameUnreachable(CenterCell);
Assert.True(lifetime.Physics.TryGetWorldFrameOffset(
CenterCell,
out _,
out _));
}
///
/// #284: a missing world frame used to report itself as
/// RetrySetupUnavailable, sending anyone diagnosing a parked placement to
/// the asset pipeline rather than the absent local-player Create. Both
/// remain retryable - only the reported cause differs.
///
[Fact]
public void ParkReasons_AreDistinctAndBothRetryable()
{
Assert.True(RuntimeSetPositionMoverPreparationStatus
.RetrySetupUnavailable.IsRetryable());
Assert.True(RuntimeSetPositionMoverPreparationStatus
.RetryWorldFrameUnavailable.IsRetryable());
Assert.False(RuntimeSetPositionMoverPreparationStatus
.RejectedAuthority.IsRetryable());
Assert.False(RuntimeSetPositionMoverPreparationStatus
.InvalidData.IsRetryable());
Assert.False(RuntimeSetPositionMoverPreparationStatus
.Prepared.IsRetryable());
Assert.Equal(
RuntimeSetPositionParkReason.AwaitingSetupCollision,
RuntimeSetPositionMoverPreparationStatus.RetrySetupUnavailable
.ParkReason());
Assert.Equal(
RuntimeSetPositionParkReason.AwaitingWorldFrame,
RuntimeSetPositionMoverPreparationStatus.RetryWorldFrameUnavailable
.ParkReason());
Assert.Equal(
RuntimeSetPositionParkReason.None,
RuntimeSetPositionMoverPreparationStatus.Prepared.ParkReason());
}
[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);
}
}