using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Runtime;
using AcDream.Runtime.Entities;
namespace AcDream.Runtime.Tests.Entities;
///
/// C5b (#275) — the steady-state accepted-Position merge is no longer a
/// residency writer. Retires AD-60's legacy half.
///
///
/// Retail's SmartBox::HandleReceivedPosition @0x00453FD0 reads the
/// wire objcell_id into a LOCAL @0x00453FE3 and hands it only to
/// BlipPlayer/TeleportPlayer/MoveOrTeleport/
/// ConstrainTo; it never assigns the object's own cell. The object's
/// cell moves inside the placement family
/// (SetPositionInternal @0x00515BD0 -> set_cell,
/// enter_world) or per-frame movement transit, and nowhere else.
///
///
///
/// TRAP T2: the withhold assertion belongs at THIS boundary
/// (), never at
/// LiveEntityNetworkUpdateController.OnPosition level — the OnPosition
/// prologue rebucket legitimately commits the wire cell a few statements
/// later (W2, the local player's own cell-freshness path, AP-146/#320), and
/// an OnPosition-level assertion would fail and tempt a weakening.
///
///
public sealed class RuntimeSteadyStatePositionMergeTests
{
private const uint Landblock = 0xA9C60000u;
private const uint LandblockSentinel = Landblock | 0xFFFFu;
private const uint Cell = Landblock | 0x0001u;
private const uint OtherCell = Landblock | 0x0002u;
///
/// C5b proof obligation 2. The pose half of the merge still lands; only
/// the derived FullCellId write is withheld, and a later canonical
/// commit still moves it.
///
[Fact]
public void AcceptedPosition_WithholdsTheWireCellAtTheMergeBoundary()
{
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
Bind(lifetime);
const uint guid = 0x80005001u;
RuntimeEntityRecord canonical =
lifetime.RegisterEntity(Spawn(guid, 1)).Canonical!;
Assert.Equal(Cell, canonical.FullCellId);
Assert.True(lifetime.TryApplyPosition(
PositionUpdate(guid, OtherCell, positionSequence: 2),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
acknowledgeProjection: null,
out PositionTimestampDisposition disposition,
out _,
out AcceptedPhysicsTimestamps timestamps));
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
// THE withhold: an accepted wire cell that differs from the committed
// cell leaves the record exactly where the last commit put it.
Assert.Equal(Cell, canonical.FullCellId);
Assert.NotEqual(OtherCell, canonical.FullCellId);
// 4b-3's D1 threading now equals a structural property of the record
// through the whole classification window.
Assert.Equal(Cell, timestamps.PreMergeCommittedCellId);
// The POSE half of the merge is emphatically not withheld.
Assert.Equal(
OtherCell,
canonical.Snapshot.Position!.Value.LandblockId);
Assert.Equal(30f, canonical.Snapshot.Position!.Value.PositionX);
Assert.Equal((ushort)2, canonical.Snapshot.PositionSequence);
// A canonical commit — the only kind of writer retail allows — still
// changes residency.
Assert.True(lifetime.CommitRebucket(
canonical,
OtherCell,
LandblockSentinel));
Assert.Equal(OtherCell, canonical.FullCellId);
}
///
/// C5b proof obligation 3 — publish-delta conservation across the moved
/// edge (trap T3). Pre-C5b the merge itself published the
/// Rebucketed delta and propagated to children, and the prologue
/// rebucket then early-returned publish-less because the cell already
/// matched. Post-C5b the merge publishes Updated and the prologue
/// rebucket's CommitRebucket publishes the Rebucketed:
/// same kind, same value, same publisher, a later point in the same call.
/// Exactly ONE of each, never zero and never two — pinned at the
/// observable, not at the source site.
///
/// C5b review D4-round fix (2026-08-05): this test used to pass
/// identically with C5b reverted and therefore proved nothing. Its
/// only delta assertion was Assert.Single(deltas, d => Rebucketed
/// && parentGuid), which FILTERS — so the pre-C5b world (merge
/// publishes Rebucketed, CommitRebucket then early-returns
/// publish-less because the cell already matches) and the post-C5b world
/// (merge publishes Updated, CommitRebucket publishes Rebucketed)
/// both satisfy it. The child arm was equally blind:
/// childSpatialBefore + 1UL holds in both worlds because whichever
/// site does NOT move the cell propagates idempotently. The discriminating
/// observable is the TOTAL ordered parent delta stream:
/// [Rebucketed] before, [Updated, Rebucketed] after.
///
/// Both parent classes per the #319 discipline: a test population
/// that only ever sees creature parents is blind to the player class.
///
///
[Theory]
[InlineData(0x50005101u, 0x50005102u)]
[InlineData(0x80005201u, 0x80005202u)]
public void CellChangingAcceptedPosition_ConservesOneRebucketAndOneChildPropagation(
uint parentGuid,
uint childGuid)
{
using RuntimeEntityObjectLifetime lifetime = EngineLifetime();
Bind(lifetime);
RuntimeEntityRecord parent =
lifetime.RegisterEntity(Spawn(parentGuid, 1)).Canonical!;
lifetime.RegisterEntity(Spawn(childGuid, 1, includePosition: false));
Assert.True(CommitAttachment(lifetime, parentGuid, 1, childGuid, 2));
Assert.True(lifetime.Entities.TryGetActive(
childGuid, out RuntimeEntityRecord child));
Assert.Equal(Cell, child.FullCellId);
ulong childSpatialBefore = child.SpatialAuthorityVersion;
var deltas = new List();
using IDisposable subscription = lifetime.Events.Subscribe(
new RecordingEntityObserver(deltas));
// The production steady-state sequence, in production order: the
// merge, then the OnPosition prologue rebucket (W2) at the wire cell.
Assert.True(lifetime.TryApplyPosition(
PositionUpdate(parentGuid, OtherCell, positionSequence: 3),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
acknowledgeProjection: null,
out PositionTimestampDisposition disposition,
out _,
out _));
Assert.Equal(PositionTimestampDisposition.Apply, disposition);
Assert.True(lifetime.CommitRebucket(parent, OtherCell, LandblockSentinel));
Assert.Equal(OtherCell, parent.FullCellId);
// THE discriminating assertion: the complete ordered parent stream.
// The merge publishes Updated (it moved no cell); the prologue
// rebucket publishes the Rebucketed. Reverting C5b's
// `refreshPosition: false` collapses this to the single-element
// [Rebucketed] and this assertion fails on both count and order.
List parentDeltas = deltas
.Where(d => d.Entity.Identity.ServerGuid == parentGuid)
.ToList();
Assert.Equal(
new[]
{
RuntimeEntityChange.Updated,
RuntimeEntityChange.Rebucketed,
},
parentDeltas.Select(d => d.Change));
// The Updated half carries the OLD cell alongside the NEW wire
// position — a torn pair that did not exist pre-C5b, because pre-C5b
// the merge stamped both together. Filed as AP-147.
Assert.Equal(Cell, parentDeltas[0].Entity.CellId);
Assert.Equal(
OtherCell,
parentDeltas[0].Entity.Position!.Value.ObjCellId);
Assert.Equal(OtherCell, parentDeltas[1].Entity.CellId);
Assert.Equal(
OtherCell,
parentDeltas[1].Entity.Position!.Value.ObjCellId);
// #319's child-equality channel: exactly one propagation, and the
// child lands on the parent's new cell.
Assert.Equal(OtherCell, child.FullCellId);
Assert.Equal(childSpatialBefore + 1UL, child.SpatialAuthorityVersion);
}
private sealed class RecordingEntityObserver(List destination)
: IRuntimeEntityObjectObserver
{
public void OnEntity(in RuntimeEntityDelta delta) => destination.Add(delta);
public void OnInventory(in RuntimeInventoryDelta delta)
{
}
}
private static RuntimeEntityObjectLifetime EngineLifetime()
{
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
engine.AddLandblock(
Landblock,
new TerrainSurface(new byte[81], new float[256]),
Array.Empty(),
Array.Empty(),
worldOffsetX: 0f,
worldOffsetY: 0f);
return new RuntimeEntityObjectLifetime(engine);
}
private static void Bind(RuntimeEntityObjectLifetime lifetime)
{
var token = new RuntimeGenerationToken(1UL);
lifetime.BindEventContext(() => token, static () => 1UL);
}
private static bool CommitAttachment(
RuntimeEntityObjectLifetime lifetime,
uint parentGuid,
ushort parentInstance,
uint childGuid,
ushort childPositionSequence)
{
var relation = new ParentAttachmentRelation(
parentGuid,
childGuid,
ParentLocation: 0u,
PlacementId: 0u,
parentInstance,
childPositionSequence);
lifetime.Entities.ParentAttachments.AcceptCreateObjectRelation(relation);
var update = new ParentEvent.Parsed(
parentGuid,
childGuid,
0u,
0u,
parentInstance,
childPositionSequence);
if (!lifetime.TryApplyParent(update, acknowledgeProjection: null, out _))
return false;
if (!lifetime.TryCommitParent(relation, acknowledgeProjection: null, out _))
return false;
if (!lifetime.Entities.ParentAttachments.CommitProjection(relation))
return false;
if (!lifetime.Entities.TryGetActive(
childGuid,
out RuntimeEntityRecord canonical))
{
return false;
}
return lifetime.CommitAcceptedParentCellless(
canonical,
canonical.PositionAuthorityVersion,
acknowledgeProjection: null);
}
private static WorldSession.EntityPositionUpdate PositionUpdate(
uint guid,
uint cell,
ushort positionSequence) =>
new(
guid,
new CreateObject.ServerPosition(cell, 30f, 20f, 7f, 1f, 0f, 0f, 0f),
Velocity: null,
PlacementId: null,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: positionSequence,
TeleportSequence: 0,
ForcePositionSequence: 0);
private static WorldSession.EntitySpawn Spawn(
uint guid,
ushort incarnation,
bool includePosition = true)
{
CreateObject.ServerPosition? position = includePosition
? new CreateObject.ServerPosition(Cell, 10f, 20f, 7f, 1f, 0f, 0f, 0f)
: null;
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 1,
State: 1,
Vector: 1,
Teleport: 0,
ServerControlledMove: 1,
ForcePosition: 0,
ObjDesc: 1,
Instance: incarnation);
var physics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.Gravity,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: null,
MotionTableId: null,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
position,
SetupTableId: null,
Array.Empty(),
Array.Empty(),
Array.Empty(),
null,
null,
"fixture",
null,
null,
null,
PhysicsState: (uint)PhysicsStateFlags.Gravity,
InstanceSequence: incarnation,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
}