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); } }