using System.Numerics; using AcDream.App.Input; using AcDream.App.Streaming; using AcDream.App.World; using AcDream.App.Net; using AcDream.App.Physics; using AcDream.App.Rendering.Vfx; using AcDream.Core.Net; using AcDream.Core.Net.Messages; using AcDream.Core.Physics; using AcDream.Core.Plugins; using AcDream.Core.World; using AcDream.Runtime; using AcDream.Runtime.Entities; using AcDream.Runtime.Physics; using AcDream.Runtime.World; using DatReaderWriter.DBObjs; namespace AcDream.App.Tests.World; /// /// C5a §5.1 (2026-08-05) — the #318 composition test, landed WITH its fix /// (coordinator override of the contract's original "red branch, fix lands /// separately" plan — every commit in this campaign stays green). Drives a /// REAL portal arrival through /// AND the REAL / /// pair, closing the gap every prior sink test left open: those tests never /// wired a real at all, so they could only ever /// assert on 's dedup cache — which /// AP-145's bug pre-seeded regardless of whether the real collision shadow /// moved. /// /// /// AP-145, RETIRED by this commit: /// RuntimePlacementPresentationSink.TryPublishPlace used to call /// _localPlayerShadow.Set(...) directly — a pure cache write — instead /// of routing through , /// the ONLY code path that calls ShadowPositionSynchronizer.Sync → /// ShadowObjectRegistry.UpdatePosition, the real collision-shadow /// publish. Two things went wrong together: (1) the portal jump's real /// shadow entry never moved to the destination, and (2) the direct cache /// write PRE-SEEDED SyncPose's own dedup check, so even a later /// per-tick SyncPose call would see "nothing changed" and skip the /// publish it would otherwise have performed. A cache-only assertion — the /// shape every prior sink test used — was satisfied by the bug: the cache /// said the right thing, only the real registry didn't. /// /// /// /// The fix: TryPublishPlace now calls /// _localPlayerShadowSync.SyncPose(entity, entity.Position, entity.Rotation, /// record.FullCellId, force: true) — the SAME publisher ordinary per-tick /// movement uses, constructed once and shared (composition root: /// LivePresentationComposition.cs now builds the ONE /// instance BEFORE the sink and /// threads it through LivePresentationResult to /// SessionPlayerComposition.cs, which no longer constructs its own). /// force: true because this is the authoritative placement commit, /// not an ordinary per-tick refresh — it must never be skipped by the dedup /// path. (which /// UpdatePosition calls internally) deregisters every prior cell row /// for the entity before adding the new ones, so the SOURCE cell's row is /// replaced, not duplicated — verified explicitly below. /// /// /// /// Architecture review A2: the exact mirror asymmetry lived six lines /// below the Place fix, on TryPublishWithdrawal — a bare /// _localPlayerShadow.Clear() with no ShadowObjects.Suspend, /// leaving a LIVE phantom collision row at the park's source cell for the /// whole park window (the #184 shape). Fixed in this same commit, in the /// same one-call shape: TryPublishWithdrawal now calls /// _localPlayerShadowSync.Suspend(entity). /// /// /// /// Architecture review A1: the original route-7-P4 test /// () /// never registered the non-player entity, so /// ShadowObjectRegistry.UpdatePosition's own not-registered early /// return made every assertion pass whether or not the player-only gate /// existed — it was vacuous on both branches. Corrected to establish a REAL /// baseline registration first, so removing the gate would actually move the /// row and actually pollute the player's cache. /// /// /// /// Sabotage-verified (manual, all four facts, both directions): with /// each fix/gate reverted in turn, the corresponding fact fails at exactly /// its discriminating assertion; with the fix applied, all four are green. /// /// public sealed class RuntimePlacementShadowCompositionTests { private const uint SourceCell = 0x01010001u; private const uint DestinationCell = 0x01020001u; private const uint Guid = 0x7000A101u; private static readonly Vector3 SourcePosition = new(10f, 10f, 5f); // Landblock-local (10,10) relative to the destination landblock's // worldOffsetX=192 — falls in the same grid cell (0x0001, cx=0,cy=0, // TerrainSurface.CellSize=24) as SourcePosition does in ITS landblock, // so ShadowObjectRegistry's flood actually lands under DestinationCell. private static readonly Vector3 DestinationPosition = new(202f, 10f, 5f); private static readonly Vector3 WirePoseDoubleWrite = new(-900f, -900f, -900f); private static readonly Quaternion DestinationOrientation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f); /// /// The discriminating composition. Establishes a REAL baseline shadow /// registration at the source pose (proving the synchronizer mechanism /// works in general), drives a real portal arrival through the sink /// (T8: with a tolerated "wire pose" double-write already sitting on the /// entity, exactly like LiveEntityNetworkUpdateController's early /// generic write — the committed suffix must overwrite it), then checks /// the dedup cache (informational negative control — would pass even /// under the AP-145 bug, kept to make the sabotage argument /// self-contained), the REAL /// registry at the destination (the load-bearing assertion — this is /// what the fix makes true), AND that the source cell's row is gone, not /// duplicated. /// [Fact] public void Place_PublishesRealPhysicsShadowAtDestination_NotOnlyTheDedupCache() { Fixture fixture = Fixture.Create(); LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell)); WorldEntity entity = Assert.IsType(record.WorldEntity); // Baseline: establish the player's REAL collision shadow at the // source pose exactly like ordinary world entry does, and prove the // synchronizer mechanism is not itself broken (sanity, not the gate). fixture.Physics.ShadowObjects.Register( entity.Id, gfxObjId: entity.SourceGfxObjOrSetupId, worldPos: SourcePosition, rotation: Quaternion.Identity, radius: 0.48f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: SourceCell & 0xFFFF0000u, collisionType: ShadowCollisionType.Sphere, cylHeight: 1.835f, seedCellId: SourceCell); fixture.Synchronizer.Sync(entity, SourceCell, force: true); Assert.Contains( fixture.Physics.ShadowObjects.GetObjectsInCell(SourceCell), e => e.EntityId == entity.Id); // T8 (route 3 §8 item 9): model the tolerated generic render-pose // write (LiveEntityNetworkUpdateController.cs:2284-2314) landing on // the entity BEFORE the committed placement suffix runs. The // intermediate "wire pose" must never leak past the Place edge. entity.Position = WirePoseDoubleWrite; entity.Rotation = Quaternion.Identity; // A real portal arrival through the canonical placement authority. RuntimePortalPlacementAuthority portal = fixture.BeginPortal( DestinationCell, teleportSequence: 1); record.FullCellId = DestinationCell; record.CanonicalLandblockId = (DestinationCell & 0xFFFF0000u) | 0xFFFFu; record.Canonical.AdvancePlacementCommit(); RuntimePlacementProjectionSnapshot place = Placement( fixture, record, portal, DestinationPosition, DestinationOrientation); Assert.True(fixture.Sink.TryApply(in place)); // T8 END STATE: the committed suffix's resolved pose wins — the // intermediate wire-pose write never leaks past the Place edge. Assert.Equal(DestinationPosition, entity.Position); Assert.Equal(DestinationOrientation, entity.Rotation); Assert.Equal(DestinationCell, entity.ParentCellId); // NEGATIVE CONTROL — deliberately NOT the gate. This is the exact // assertion shape every prior sink test uses // (RuntimePlacementPresentationSinkTests.Place_Reframes...); it // would pass EVEN under the AP-145 bug, since the cache is what the // buggy direct write updated. Kept here to make the sabotage // argument self-contained: if this assertion were the only // coverage, the bug would have been invisible. Assert.Equal( new LocalPlayerShadowState.Snapshot( DestinationPosition, DestinationOrientation, DestinationCell), fixture.LocalShadow.Current); // THE DISCRIMINATING ASSERTION — this is what AP-145's bug broke and // the fix restores: the real collision shadow used by every OTHER // entity's collision sweep now actually followed the portal jump. Assert.Contains( fixture.Physics.ShadowObjects.GetObjectsInCell(DestinationCell), e => e.EntityId == entity.Id); // No double-publish, no stale row left behind: Register/UpdatePosition // deregisters every prior cell row before adding the new ones, so the // SOURCE cell must carry zero rows for this entity now. Assert.DoesNotContain( fixture.Physics.ShadowObjects.GetObjectsInCell(SourceCell), e => e.EntityId == entity.Id); } /// /// The second half of AP-145's mechanism, and the second half of the /// fix's proof: the OLD direct cache write did not merely skip the /// publish once — it pre-seeded /// 's own dedup /// check, so even a SUBSEQUENT ordinary per-tick Sync call (the /// thing that would normally self-heal a one-frame miss) saw "nothing /// changed" and skipped the publish too. With the fix, TryPublishPlace /// itself now runs the real publish through the SAME synchronizer, so /// the subsequent ordinary tick's dedup skip is no longer a bug — it is /// CORRECTLY a no-op, because the real registry is already right. /// [Fact] public void Place_ThenOrdinaryTick_DoesNotNeedToSelfHeal_RealShadowAlreadyRight() { Fixture fixture = Fixture.Create(); LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell)); WorldEntity entity = Assert.IsType(record.WorldEntity); fixture.Physics.ShadowObjects.Register( entity.Id, gfxObjId: entity.SourceGfxObjOrSetupId, worldPos: SourcePosition, rotation: Quaternion.Identity, radius: 0.48f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: SourceCell & 0xFFFF0000u, collisionType: ShadowCollisionType.Sphere, cylHeight: 1.835f, seedCellId: SourceCell); fixture.Synchronizer.Sync(entity, SourceCell, force: true); RuntimePortalPlacementAuthority portal = fixture.BeginPortal( DestinationCell, teleportSequence: 1); record.FullCellId = DestinationCell; record.CanonicalLandblockId = (DestinationCell & 0xFFFF0000u) | 0xFFFFu; record.Canonical.AdvancePlacementCommit(); RuntimePlacementProjectionSnapshot place = Placement( fixture, record, portal, DestinationPosition, DestinationOrientation); Assert.True(fixture.Sink.TryApply(in place)); // Simulate the very next ordinary frame's shadow-sync tick — exactly // what production runs every frame for the local player. Not forced: // this is the real per-tick call shape, dedup and all. It is // EXPECTED to be a no-op now: TryPublishPlace's own SyncPose call // already did the real work, so the cache already matches. fixture.Synchronizer.Sync(entity, DestinationCell); Assert.Contains( fixture.Physics.ShadowObjects.GetObjectsInCell(DestinationCell), e => e.EntityId == entity.Id); Assert.DoesNotContain( fixture.Physics.ShadowObjects.GetObjectsInCell(SourceCell), e => e.EntityId == entity.Id); // Exactly one row for this entity anywhere — the ordinary tick's // dedup no-op did not create a second one. Assert.Single( fixture.Physics.ShadowObjects.AllEntriesForDebug(), e => e.EntityId == entity.Id); } /// /// Route 7 P4 still binds: a committed CHILD (or any non-local-player /// entity) must not gain a broadphase row, and must not have its OWN /// pose written into the PLAYER's dedup cache. The fix lives entirely /// inside TryPublishPlace's pre-existing /// record.ServerGuid == _localPlayerGuid() gate — unchanged by /// this fix, only what runs INSIDE it changed. /// /// /// C5a architecture review A1 (2026-08-05): the FIRST version of this /// test registered nothing for the child, so /// ShadowObjectRegistry.UpdatePosition's own /// not-registered early return (ShadowObjectRegistry.cs:696) made /// every assertion pass whether or not the gate existed — removing the /// gate was unobservable because the sabotaged code path was ALSO a /// no-op. This version establishes a REAL baseline registration for the /// child first (mirroring fact 1's own baseline), so a gate-removed /// sabotage would actually move the child's row and would actually /// pollute the player's cache — both of which the assertions below now /// check directly, not by absence of registration. /// /// [Fact] public void Place_ForNonLocalPlayerEntity_NeverTouchesShadowObjects() { const uint ChildGuid = 0x7000A102u; Fixture fixture = Fixture.Create(); LiveEntityRecord record = fixture.Materialize(Spawn(ChildGuid, 1, SourceCell)); WorldEntity entity = Assert.IsType(record.WorldEntity); Assert.NotEqual(Guid, ChildGuid); // REAL baseline: the child has its own genuine collision shadow at // the source pose, exactly like fact 1 establishes for the player. // A sabotaged (gate-removed) TryPublishPlace WOULD move this row to // the destination and WOULD write the child's pose into the // player's LocalShadow cache — both are asserted against below. fixture.Physics.ShadowObjects.Register( entity.Id, gfxObjId: entity.SourceGfxObjOrSetupId, worldPos: SourcePosition, rotation: Quaternion.Identity, radius: 0.48f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: SourceCell & 0xFFFF0000u, collisionType: ShadowCollisionType.Sphere, cylHeight: 1.835f, seedCellId: SourceCell); Assert.Contains( fixture.Physics.ShadowObjects.GetObjectsInCell(SourceCell), e => e.EntityId == entity.Id); RuntimePortalPlacementAuthority portal = fixture.BeginPortal( DestinationCell, teleportSequence: 1); record.FullCellId = DestinationCell; record.CanonicalLandblockId = (DestinationCell & 0xFFFF0000u) | 0xFFFFu; record.Canonical.AdvancePlacementCommit(); RuntimePlacementProjectionSnapshot place = Placement( fixture, record, portal, DestinationPosition, DestinationOrientation); Assert.True(fixture.Sink.TryApply(in place)); // The render entity DID move (Place still works for a non-player // entity) — only the shadow-publish branch is player-gated. Assert.Equal(DestinationPosition, entity.Position); // THE DISCRIMINATING ASSERTIONS. Under a sabotaged (gate-removed) // TryPublishPlace, SyncPose would run for the child: the child's own // record IS its own current visible projection, so // IsCurrentVisibleProjection would be true and the real publish // would execute — moving the row to DestinationCell and vacating // SourceCell (exactly what fact 1 asserts is CORRECT for the // player). Here it must NOT happen — this is what tells the // sabotage apart from the fix. Assert.Contains( fixture.Physics.ShadowObjects.GetObjectsInCell(SourceCell), e => e.EntityId == entity.Id); Assert.DoesNotContain( fixture.Physics.ShadowObjects.GetObjectsInCell(DestinationCell), e => e.EntityId == entity.Id); Assert.Equal(1, fixture.Physics.ShadowObjects.TotalRegistered); // The player's OWN dedup cache must stay untouched by a non-player // Place — a sabotaged TryPublishPlace would call // _localPlayerShadowSync.SyncPose using the CHILD's guid check but // the PLAYER's cache field (the gate exists to prevent exactly a // non-player entity's pose from being written where the player's // pose belongs). Assert.Null(fixture.LocalShadow.Current); } /// /// Architecture review A2 (2026-08-05): the exact mirror of AP-145's /// Place-side bug lived six lines below it, on the Withdraw path /// — TryPublishWithdrawal cleared only the dedup cache, leaving a /// LIVE phantom row in at the /// park's source cell for the whole park window (the #184 shape: every /// other entity's collision sweep in that cell would collide with a /// player who is, per every other acdream predicate, gone). Fixed in the /// same commit as the Place half, via the SAME one-call shape: /// TryPublishWithdrawal now calls /// _localPlayerShadowSync.Suspend(entity), which does the real /// registry suspend AND the cache clear together. /// [Fact] public void Withdraw_SuspendsRealPhysicsShadow_NotOnlyTheDedupCache() { Fixture fixture = Fixture.Create(); LiveEntityRecord record = fixture.Materialize(Spawn(Guid, 1, SourceCell)); WorldEntity entity = Assert.IsType(record.WorldEntity); // Baseline: a real collision shadow at the source pose, exactly like // ordinary world entry / fact 1 establishes. fixture.Physics.ShadowObjects.Register( entity.Id, gfxObjId: entity.SourceGfxObjOrSetupId, worldPos: SourcePosition, rotation: Quaternion.Identity, radius: 0.48f, worldOffsetX: 0f, worldOffsetY: 0f, landblockId: SourceCell & 0xFFFF0000u, collisionType: ShadowCollisionType.Sphere, cylHeight: 1.835f, seedCellId: SourceCell); fixture.Synchronizer.Sync(entity, SourceCell, force: true); Assert.Contains( fixture.Physics.ShadowObjects.GetObjectsInCell(SourceCell), e => e.EntityId == entity.Id); Assert.NotNull(fixture.LocalShadow.Current); RuntimePlacementProjectionSnapshot withdraw = Placement( fixture, record, portal: default, entity.Position, entity.Rotation, RuntimePlacementProjectionKind.Withdraw); Assert.True(fixture.Sink.TryApply(in withdraw)); // THE DISCRIMINATING ASSERTIONS. Under the pre-fix bug, the cache // clears (this passes either way) but the real registry keeps the // SOURCE-cell row (a live phantom for the park's duration) — that is // what the sabotage below must be able to catch. Assert.Null(fixture.LocalShadow.Current); Assert.DoesNotContain( fixture.Physics.ShadowObjects.GetObjectsInCell(SourceCell), e => e.EntityId == entity.Id); // Suspend removes the entity from every cell bucket (TotalRegistered // — what any collision sweep can find) but is deliberately NOT // logical teardown (ShadowObjectRegistry.Suspend's own xmldoc: "the // registry counterpart of retail CPhysicsObj::remove_shadows_from_cells // during temporary leave-world/pending-cell residence"), so the // RETAINED registration survives for TryPublishPlace's later // force:true SyncPose (WithdrawalRestored, or a fresh Place) to // re-publish from. Assert.Equal(0, fixture.Physics.ShadowObjects.TotalRegistered); Assert.Equal(1, fixture.Physics.ShadowObjects.RetainedRegistrationCount); } private static RuntimePlacementProjectionSnapshot Placement( Fixture fixture, LiveEntityRecord record, RuntimePortalPlacementAuthority portal, Vector3 position, Quaternion orientation, RuntimePlacementProjectionKind kind = RuntimePlacementProjectionKind.Place) { RuntimeEntityRecord canonical = record.Canonical; var token = new RuntimePlacementProjectionToken( Sequence: 1, Revision: 1, Entity: record.ProjectionKey!.Value, PositionAuthorityVersion: canonical.PositionAuthorityVersion, SpatialAuthorityVersion: canonical.SpatialAuthorityVersion, PlacementCommitVersion: canonical.PlacementCommitVersion, SessionLifetimeVersion: fixture.Runtime.SessionLifetimeVersion, ExactCellId: canonical.FullCellId, CollisionGeneration: 1, Portal: portal); return new RuntimePlacementProjectionSnapshot( token, kind, position, orientation, CellLocalPosition: position, InContact: false, OnWalkable: false); } private static WorldSession.EntitySpawn Spawn( uint guid, ushort instance, uint cell) { var position = new CreateObject.ServerPosition( cell, 10f, 10f, 5f, 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: instance); var physics = new PhysicsSpawnData( RawState: (uint)PhysicsStateFlags.ReportCollisions, Position: position, Movement: null, AnimationFrame: null, SetupTableId: 0x02000001u, MotionTableId: 0x09000001u, 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, 0x02000001u, Array.Empty(), Array.Empty(), Array.Empty(), null, null, "fixture", null, null, 0x09000001u, PhysicsState: (uint)PhysicsStateFlags.ReportCollisions, InstanceSequence: instance, MovementSequence: 1, ServerControlSequence: 1, PositionSequence: 1, Physics: physics); } private sealed class Fixture { private Fixture( PhysicsEngine physics, GpuWorldState spatial, LiveEntityRuntime runtime, RuntimeWorldTransitState transit, WorldGameState worldState, WorldEvents worldEvents, EntityEffectPoseRegistry effectPoses, LocalPlayerShadowState localShadow, LocalPlayerShadowSynchronizer synchronizer) { Physics = physics; Spatial = spatial; Runtime = runtime; Transit = transit; WorldState = worldState; WorldEvents = worldEvents; EffectPoses = effectPoses; LocalShadow = localShadow; Synchronizer = synchronizer; Sink = new RuntimePlacementPresentationSink( runtime, transit, worldState, worldEvents, effectPoses, synchronizer, () => Guid, _ => { }, [(_, _) => { }]); } internal PhysicsEngine Physics { get; } internal GpuWorldState Spatial { get; } internal LiveEntityRuntime Runtime { get; } internal RuntimeWorldTransitState Transit { get; } internal WorldGameState WorldState { get; } internal WorldEvents WorldEvents { get; } internal EntityEffectPoseRegistry EffectPoses { get; } internal LocalPlayerShadowState LocalShadow { get; } internal LocalPlayerShadowSynchronizer Synchronizer { get; } internal RuntimePlacementPresentationSink Sink { get; } internal static Fixture Create() { var physics = new PhysicsEngine { DataCache = new PhysicsDataCache() }; physics.AddLandblock( SourceCell & 0xFFFF0000u, new TerrainSurface(new byte[81], new float[256]), Array.Empty(), Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); physics.AddLandblock( DestinationCell & 0xFFFF0000u, new TerrainSurface(new byte[81], new float[256]), Array.Empty(), Array.Empty(), worldOffsetX: 192f, worldOffsetY: 0f); var spatial = new GpuWorldState(); spatial.AddLandblock(EmptyLandblock(SourceCell | 0xFFFFu)); spatial.AddLandblock(EmptyLandblock(DestinationCell | 0xFFFFu)); var resources = new RecordingResources(); LiveEntityRuntime runtime = LiveEntityRuntimeFixture.Create( spatial, resources, physics); var identity = new LocalPlayerIdentityState { ServerGuid = Guid }; var origin = new LiveWorldOriginState(); origin.SetPlaceholder(0, 0); var localShadow = new LocalPlayerShadowState(); var synchronizer = new LocalPlayerShadowSynchronizer( physics, runtime, identity, origin, localShadow); return new Fixture( physics, spatial, runtime, new RuntimeWorldTransitState(), new WorldGameState(), new WorldEvents(), new EntityEffectPoseRegistry(), localShadow, synchronizer); } internal LiveEntityRecord Materialize(WorldSession.EntitySpawn spawn) { LiveEntityRecord record = Runtime.RegisterAndMaterializeProjection(spawn); Assert.False(Runtime.HasActiveInitialCreateResidence( record.Canonical)); Assert.True(record.ResourcesRegistered); WorldEntity entity = record.WorldEntity!; var snapshot = new AcDream.Plugin.Abstractions.WorldEntitySnapshot( entity.Id, entity.SourceGfxObjOrSetupId, entity.Position, entity.Rotation); WorldState.Add(snapshot); WorldEvents.UpsertCurrent(snapshot); EffectPoses.PublishMeshRefs(entity); return record; } internal RuntimePortalPlacementAuthority BeginPortal( uint cell, ushort teleportSequence) { Assert.True(Transit.TryQueueTeleportStart(teleportSequence)); Assert.True(Transit.ActivateQueuedTeleport()); Assert.True(Transit.OfferTeleportDestination( new RuntimeTeleportDestination( Guid, InstanceSequence: 1, PositionSequence: 1, TeleportSequence: teleportSequence, ForcePositionSequence: 1, new Position( cell, new Vector3(1f, 2f, 3f), Quaternion.Identity)), teleportTimestampAdvanced: true)); Assert.True(Transit.TryBeginPortalReveal( teleportSequence, cell, out long generation)); Assert.True(Transit.TryRegisterHostProjection( generation, cell, out RuntimeWorldHostProjectionToken host)); return new RuntimePortalPlacementAuthority( true, generation, teleportSequence, host); } private static LoadedLandblock EmptyLandblock(uint canonicalId) => new(canonicalId, new LandBlock(), Array.Empty()); } private sealed class RecordingResources : ILiveEntityResourceLifecycle { public void Register(WorldEntity entity) { } public void Unregister(WorldEntity entity) { } } }