fix(interaction): restore retail loot placement and world-drop projection

This commit is contained in:
Erik 2026-07-27 00:03:15 +02:00
parent 4d095be286
commit 921712f412
24 changed files with 1581 additions and 34 deletions

View file

@ -17,6 +17,7 @@ public sealed class SelectionInteractionControllerTests
{
private const uint Player = 0x5000_0001u;
private const uint Target = 0x7000_0001u;
private const uint GroundContainer = 0x7000_0010u;
private sealed class Query : IWorldSelectionQuery
{
@ -140,6 +141,7 @@ public sealed class SelectionInteractionControllerTests
public readonly List<PendingBackpackPlacement> CancelledPlacements = new();
public readonly ItemInteractionController Items;
public readonly SelectionInteractionController Controller;
public uint GroundObjectId { get; set; }
public Harness()
{
@ -173,6 +175,7 @@ public sealed class SelectionInteractionControllerTests
Query.Events.Add("examine");
Examines.Add(guid);
},
groundObjectId: () => GroundObjectId,
placeInBackpack: (item, container, placement) =>
controller!.SendPickup(item, container, placement),
requestUse: (guid, reservation) =>
@ -495,6 +498,32 @@ public sealed class SelectionInteractionControllerTests
Assert.Equal(new[] { (Target, Player, 0) }, h.Transport.Pickups);
}
[Fact]
public void CorpseChildDoubleClickDispatchesWithoutAWorldProjection()
{
const uint loot = 0x7000_0011u;
var h = new Harness
{
GroundObjectId = GroundContainer,
};
h.Objects.AddOrUpdate(new ClientObject
{
ObjectId = loot,
Name = "Corpse loot",
Type = ItemType.Misc,
ContainerId = GroundContainer,
});
h.Query.Pickupable = false;
h.Query.Approach = null;
Assert.True(h.Items.ActivateItem(loot));
Assert.Empty(h.Movement.Approaches);
Assert.Equal(new[] { (loot, Player, 0) }, h.Transport.Pickups);
Assert.Single(h.PendingPlacements);
Assert.Empty(h.CancelledPlacements);
}
[Fact]
public void SessionResetClearsQueuedAndDeferredWork()
{

View file

@ -319,6 +319,75 @@ public sealed class WorldSelectionQueryTests
Assert.Equal(4f, radius);
}
[Fact]
public void VividTargetMarkerWaitsForFreshProjectionWhenDropMovePrecedesPosition()
{
var h = new Harness();
WorldEntity entity =
h.Add(Target, new Vector3(3f, 4f, 0f), ItemType.Misc);
Assert.NotNull(h.Query.ResolveVividTargetInfo(Target));
Assert.True(h.Objects.MoveItem(Target, Player, newSlot: 0));
Assert.True(h.Runtime.WithdrawLiveEntityProjection(Target));
Assert.Null(h.Query.ResolveVividTargetInfo(Target));
// ACE may confirm InventoryPutObjectIn3D before sending the fresh
// Position. The retained entity still has the old ground pose here.
Assert.True(h.Objects.MoveItem(Target, 0u, newSlot: -1));
Assert.Null(h.Query.ResolveVividTargetInfo(Target));
var newPosition = new Vector3(30f, 40f, 2f);
entity.SetPosition(newPosition);
Assert.True(h.Runtime.RebucketLiveEntity(Target, 0x0101_0001u));
var marker = h.Query.ResolveVividTargetInfo(Target);
Assert.NotNull(marker);
Assert.Equal(newPosition + Vector3.UnitX, marker.Value.SelectionSphereCenter);
}
[Fact]
public void VividTargetMarkerUsesFreshPoseWhenPositionPrecedesDropMove()
{
var h = new Harness();
WorldEntity entity =
h.Add(Target, new Vector3(3f, 4f, 0f), ItemType.Misc);
Assert.True(h.Objects.MoveItem(Target, Player, newSlot: 0));
Assert.True(h.Runtime.WithdrawLiveEntityProjection(Target));
var newPosition = new Vector3(30f, 40f, 2f);
entity.SetPosition(newPosition);
Assert.True(h.Runtime.RebucketLiveEntity(Target, 0x0101_0001u));
// The spatial pose is current, but ownership continues to suppress the
// marker until ServerSaysMoveItem publishes the world placement.
Assert.Null(h.Query.ResolveVividTargetInfo(Target));
Assert.True(h.Objects.MoveItem(Target, 0u, newSlot: -1));
var marker = h.Query.ResolveVividTargetInfo(Target);
Assert.NotNull(marker);
Assert.Equal(newPosition + Vector3.UnitX, marker.Value.SelectionSphereCenter);
}
[Fact]
public void VividTargetMarkerRejectsRetainedProjectionInsideExternalContainer()
{
var h = new Harness();
const uint corpse = 0x7000_0040u;
h.Add(Target, new Vector3(3f, 4f, 0f), ItemType.Misc);
h.Objects.AddOrUpdate(new ClientObject
{
ObjectId = corpse,
Name = "Corpse",
Type = ItemType.Container,
});
Assert.True(h.Objects.MoveItem(Target, corpse, newSlot: 0));
Assert.False(h.Objects.IsOwnedByObject(Target, Player));
Assert.Null(h.Query.ResolveVividTargetInfo(Target));
}
private static SelectionCameraSnapshot Camera()
{
Matrix4x4 view = Matrix4x4.CreateLookAt(

View file

@ -159,6 +159,8 @@ public sealed class LiveEntityInboundAuthorityGateTests
out AcceptedPositionNetworkUpdate accepted));
Assert.Equal(PositionTimestampDisposition.Apply, accepted.TimestampDisposition);
Assert.Equal((ulong)2, accepted.PositionAuthorityVersion);
Assert.True(runtime.TryGetCanonical(Guid, out RuntimeEntityRecord canonical));
Assert.Same(canonical, accepted.Canonical);
Assert.Equal(1, publishCount);
gate.ResetSessionState();
@ -197,6 +199,33 @@ public sealed class LiveEntityInboundAuthorityGateTests
out _));
}
[Fact]
public void Position_CanonicalInventoryObjectIsAcceptedBeforeProjectionExists()
{
LiveEntityRuntime runtime = Runtime();
LiveEntityRegistrationResult registration =
runtime.RegisterLiveEntity(Spawn(Guid, instance: 5, position: 1));
Assert.NotNull(registration.Canonical);
Assert.False(runtime.TryGetRecord(Guid, out _));
int publishCount = 0;
var gate = new LiveEntityInboundAuthorityGate(
runtime,
(_, _) => publishCount++);
Assert.True(gate.TryAcceptPosition(
Position(Guid, 5, 2, 0x01010002u),
localPlayerGuid: 0u,
forcePositionRotation: null,
currentLocalVelocity: null,
payloadIsValid: true,
out AcceptedPositionNetworkUpdate accepted));
Assert.Same(registration.Canonical, accepted.Canonical);
Assert.Equal((ulong)2, accepted.PositionAuthorityVersion);
Assert.Equal(1, publishCount);
Assert.False(runtime.TryGetRecord(Guid, out _));
}
[Fact]
public void Vector_NewerSameIncarnationPacketInvalidatesCapturedAuthority()
{
@ -308,10 +337,10 @@ public sealed class LiveEntityInboundAuthorityGateTests
payloadIsValid: true,
out AcceptedPositionNetworkUpdate accepted));
Assert.True(runtime.IsCurrentPositionAuthority(
accepted.Record,
accepted.Canonical,
accepted.PositionAuthorityVersion));
Assert.False(runtime.IsCurrentVelocityAuthority(
accepted.Record,
accepted.Canonical,
accepted.VelocityAuthorityVersion));
}

View file

@ -1051,6 +1051,47 @@ public sealed class EquippedChildProjectionWithdrawalTests
Assert.Single(fixture.Controller.AttachedEntityIds);
}
[Fact]
public void InventoryPutObjectIn3D_DoesNotWithdrawRecoveredWorldProjection()
{
int withdrawals = 0;
ControllerFixture? fixture = null;
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
{
withdrawals++;
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
record,
positionVersion,
projectionVersion);
return new(
completed
? ExactProjectionWithdrawalDisposition.Completed
: ExactProjectionWithdrawalDisposition.Superseded,
Failure: null);
});
using (fixture)
{
const uint guid = 0x7000026Au;
const uint player = 0x50000001u;
LiveEntityRecord dropped = fixture.Spawn(guid, generation: 1);
fixture.Objects.AddOrUpdate(new ClientObject
{
ObjectId = guid,
ContainerId = player,
CurrentlyEquippedLocation = EquipMask.None,
});
Assert.True(fixture.Objects.ApplyConfirmedServerMove(
guid,
newContainerId: 0u,
newWielderId: 0u));
Assert.Equal(0, withdrawals);
Assert.True(dropped.IsSpatiallyProjected);
Assert.Equal(LiveEntityProjectionKind.World, dropped.ProjectionKind);
}
}
[Fact]
public void OrphanRollback_PendingFailureRetainsExactRetryOwner()
{

View file

@ -1489,6 +1489,8 @@ public sealed class ItemInteractionControllerTests
{
var h = new Harness();
h.AddContained(0x50000A70u, item => item.StackSize = 10);
WorldDropDispatch? dispatched = null;
h.Controller.WorldDropDispatched += value => dispatched = value;
h.SelectedObject = 0x50000A70u;
h.SplitQuantity.Reset(10u);
h.SplitQuantity.SetValue(1u);
@ -1504,6 +1506,10 @@ public sealed class ItemInteractionControllerTests
Assert.Empty(h.Drops);
Assert.Equal(Pack, h.Objects.Get(0x50000A70u)!.ContainerId);
Assert.Equal(10, h.Objects.Get(0x50000A70u)!.StackSize);
Assert.NotNull(dispatched);
Assert.Equal(InventoryRequestKind.SplitToWorld, dispatched.Value.Request.Kind);
Assert.Equal(0x50000A70u, dispatched.Value.Request.ItemId);
Assert.Equal(1u, dispatched.Value.Amount);
}
[Fact]

View file

@ -3,7 +3,9 @@ using System.Reflection;
using AcDream.App.Physics;
using AcDream.App.Rendering;
using AcDream.App.World;
using AcDream.Core.Net;
using AcDream.Core.Physics;
using AcDream.Core.World;
namespace AcDream.App.Tests.World;
@ -65,6 +67,42 @@ public sealed class GameWindowLiveEntityCompositionTests
BindingFlags.NonPublic));
}
[Fact]
public void RetainedButWithdrawnWorldEntity_RequiresPositionProjectionRecovery()
{
LiveEntityRecord record =
LiveEntityTestFixture.CreateExactProjectionRecord(
new WorldSession.EntitySpawn(
Guid: 0x7000_00D0u,
Position: null,
SetupTableId: null,
AnimPartChanges: [],
TextureChanges: [],
SubPalettes: [],
BasePaletteId: null,
ObjScale: null,
Name: null,
ItemType: null,
MotionState: null,
MotionTableId: null,
InstanceSequence: 1));
record.WorldEntity = new WorldEntity
{
Id = record.LocalEntityId!.Value,
ServerGuid = record.ServerGuid,
SourceGfxObjOrSetupId = 0x0200_0001u,
Position = System.Numerics.Vector3.Zero,
Rotation = System.Numerics.Quaternion.Identity,
MeshRefs = [],
};
Assert.NotNull(record.WorldEntity);
Assert.False(record.IsSpatiallyProjected);
Assert.True(
LiveEntityNetworkUpdateController
.RequiresSpatialProjectionRecovery(record));
}
[Theory]
[InlineData(typeof(LiveEntityCollisionBuilder))]
[InlineData(typeof(LiveEntityDefaultPoseResolver))]

View file

@ -423,6 +423,85 @@ public sealed class LiveEntityHydrationControllerTests
Assert.Equal(0, fixture.Resources.UnregisterCount);
}
[Fact]
public void PositionAfterInventoryOnlyCreate_ConstructsFirstSpatialProjection()
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(CelllessSpawn(
PositionSequence: 1,
parentGuid: null));
RuntimeEntityRecord canonical = fixture.Canonical;
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
Assert.Equal(0, fixture.Resources.RegisterCount);
Assert.True(fixture.Runtime.TryApplyPosition(
PositionUpdate(sequence: 2, x: 14f),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out _,
out _));
ulong authorityVersion = canonical.PositionAuthorityVersion;
Assert.True(fixture.Controller.RecoverCanonicalProjection(
canonical,
authorityVersion,
out LiveEntityRecord record));
Assert.NotNull(record.WorldEntity);
Assert.True(record.IsSpatiallyProjected);
Assert.True(record.InitialHydrationCompleted);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(1, fixture.Ready.PublishCount);
Assert.Contains(
fixture.Materializer.Calls,
call => call.Purpose is LiveProjectionPurpose.SpatialRecovery);
Assert.True(fixture.Controller.RecoverCanonicalProjection(
canonical,
authorityVersion,
out LiveEntityRecord same));
Assert.Same(record, same);
Assert.Equal(1, fixture.Resources.RegisterCount);
Assert.Equal(1, fixture.Ready.PublishCount);
}
[Fact]
public void CanonicalProjectionRecovery_RejectsSupersededPositionAuthority()
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(CelllessSpawn(
PositionSequence: 1,
parentGuid: null));
RuntimeEntityRecord canonical = fixture.Canonical;
Assert.True(fixture.Runtime.TryApplyPosition(
PositionUpdate(sequence: 2, x: 14f),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out _,
out _));
ulong staleAuthorityVersion = canonical.PositionAuthorityVersion;
Assert.True(fixture.Runtime.TryApplyPosition(
PositionUpdate(sequence: 3, x: 18f),
isLocalPlayer: false,
forcePositionRotation: null,
currentLocalVelocity: null,
out _,
out _,
out _));
Assert.False(fixture.Controller.RecoverCanonicalProjection(
canonical,
staleAuthorityVersion,
out _));
Assert.False(fixture.Runtime.TryGetRecord(Guid, out _));
Assert.Equal(0, fixture.Resources.RegisterCount);
Assert.Equal(0, fixture.Ready.PublishCount);
}
[Fact]
public void ParentEvent_IsRetainedByRelationshipOwnerAndAcceptedThroughHydrationGate()
{
@ -2350,4 +2429,19 @@ public sealed class LiveEntityHydrationControllerTests
PlacementId: parentGuid is null ? null : 1u,
Physics: physics);
}
private static WorldSession.EntityPositionUpdate PositionUpdate(
ushort sequence,
float x) =>
new(
Guid,
new CreateObject.ServerPosition(
Cell, x, 12f, 6f, 1f, 0f, 0f, 0f),
Velocity: null,
PlacementId: 0,
IsGrounded: true,
InstanceSequence: 1,
PositionSequence: sequence,
TeleportSequence: 0,
ForcePositionSequence: 0);
}

View file

@ -0,0 +1,186 @@
using System.Numerics;
using AcDream.App.World;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
namespace AcDream.App.Tests.World;
public sealed class PendingSplitToWorldProjectionTests
{
[Fact]
public void Resolve_ClonesSourceAppearanceIntoAuthoritativeGroundPlacement()
{
var pending = new PendingSplitToWorldProjection();
WorldSession.EntitySpawn source = SourceSpawn();
pending.Record(7u, source, amount: 3u, now: 12.0);
WorldSession.EntityPositionUpdate update = PositionUpdate(
guid: 0x80000435u,
instance: 4,
position: 9,
teleport: 2,
forcePosition: 3);
Assert.True(pending.TryResolve(update, now: 12.25, out var spawn));
Assert.Equal(update.Guid, spawn.Guid);
Assert.Equal(source.SetupTableId, spawn.SetupTableId);
Assert.Equal(source.AnimPartChanges, spawn.AnimPartChanges);
Assert.Equal(source.TextureChanges, spawn.TextureChanges);
Assert.Equal(source.SubPalettes, spawn.SubPalettes);
Assert.Equal(source.BasePaletteId, spawn.BasePaletteId);
Assert.Equal(source.WeenieClassId, spawn.WeenieClassId);
Assert.Equal(3, spawn.StackSize);
Assert.Equal(0u, spawn.ContainerId);
Assert.Equal(0u, spawn.WielderId);
Assert.Equal(0u, spawn.CurrentWieldedLocation);
Assert.Null(spawn.ParentGuid);
Assert.Null(spawn.ParentLocation);
Assert.Equal(update.Position, spawn.Position);
Assert.Equal(update.PlacementId, spawn.PlacementId);
Assert.Equal(update.InstanceSequence, spawn.InstanceSequence);
Assert.Equal(update.PositionSequence, spawn.PositionSequence);
PhysicsSpawnData physics = Assert.IsType<PhysicsSpawnData>(spawn.Physics);
Assert.Equal(update.Position, physics.Position);
Assert.Null(physics.Parent);
Assert.Equal(update.Velocity, physics.Velocity);
Assert.Equal(update.InstanceSequence, physics.Timestamps.Instance);
Assert.Equal(update.PositionSequence, physics.Timestamps.Position);
Assert.Equal(update.TeleportSequence, physics.Timestamps.Teleport);
Assert.Equal(
update.ForcePositionSequence,
physics.Timestamps.ForcePosition);
Assert.False(pending.HasPending);
}
[Fact]
public void Resolve_ConsumesOneRetailPendingIdentity()
{
var pending = new PendingSplitToWorldProjection();
pending.Record(1u, SourceSpawn(), amount: 1u, now: 5.0);
Assert.True(pending.TryResolve(
PositionUpdate(0x80000435u),
now: 5.1,
out _));
Assert.False(pending.TryResolve(
PositionUpdate(0x80000436u),
now: 5.2,
out _));
}
[Fact]
public void Resolve_RejectsAtRetailTenSecondBoundary()
{
var pending = new PendingSplitToWorldProjection();
pending.Record(1u, SourceSpawn(), amount: 1u, now: 5.0);
Assert.False(pending.TryResolve(
PositionUpdate(0x80000435u),
now: 15.0,
out _));
Assert.False(pending.HasPending);
}
[Fact]
public void Resolve_SourceGuidDoesNotConsumePendingIdentity()
{
var pending = new PendingSplitToWorldProjection();
WorldSession.EntitySpawn source = SourceSpawn();
pending.Record(1u, source, amount: 1u, now: 5.0);
Assert.False(pending.TryResolve(
PositionUpdate(source.Guid),
now: 5.1,
out _));
Assert.True(pending.HasPending);
}
private static WorldSession.EntitySpawn SourceSpawn()
{
var timestamps = new PhysicsTimestamps(
Position: 1,
Movement: 2,
State: 3,
Vector: 4,
Teleport: 5,
ServerControlledMove: 6,
ForcePosition: 7,
ObjDesc: 8,
Instance: 9);
var physics = new PhysicsSpawnData(
RawState: 0x00000408u,
Position: null,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x0200030Bu,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: new PhysicsAttachment(0x50000001u, 0u),
Children: null,
Scale: 1f,
Friction: 0.5f,
Elasticity: 0.05f,
Translucency: null,
Velocity: Vector3.Zero,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
Guid: 0x50000010u,
Position: null,
SetupTableId: 0x0200030Bu,
AnimPartChanges: Array.Empty<CreateObject.AnimPartChange>(),
TextureChanges: Array.Empty<CreateObject.TextureChange>(),
SubPalettes: Array.Empty<CreateObject.SubPaletteSwap>(),
BasePaletteId: 0x04000001u,
ObjScale: 1f,
Name: "Copper Pea",
ItemType: 0x00000001u,
MotionState: null,
MotionTableId: 0x09000001u,
WeenieClassId: 273u,
StackSize: 100,
StackSizeMax: 1000,
ContainerId: 0x50000001u,
WielderId: 0u,
CurrentWieldedLocation: 0u,
InstanceSequence: 9,
PositionSequence: 1,
ParentGuid: 0x50000001u,
ParentLocation: 0u,
Physics: physics);
}
private static WorldSession.EntityPositionUpdate PositionUpdate(
uint guid,
ushort instance = 4,
ushort position = 9,
ushort teleport = 2,
ushort forcePosition = 3)
{
var serverPosition = new CreateObject.ServerPosition(
0xA9B4001Au,
12f,
34f,
1.5f,
1f,
0f,
0f,
0f);
return new WorldSession.EntityPositionUpdate(
guid,
serverPosition,
Velocity: Vector3.Zero,
PlacementId: 0x65u,
IsGrounded: true,
InstanceSequence: instance,
PositionSequence: position,
TeleportSequence: teleport,
ForcePositionSequence: forcePosition);
}
}

View file

@ -46,6 +46,49 @@ public sealed class ClientObjectTableTests
Assert.Equal(1, propUpdateCount);
}
[Fact]
public void IsOwnedByObjectMatchesRetailDirectPackAndLocationRules()
{
const uint player = 0x5000_0001u;
const uint pack = 0x4000_0001u;
var repo = new ClientObjectTable();
repo.AddOrUpdate(MakeItem(player, "Player"));
repo.AddOrUpdate(new ClientObject
{
ObjectId = pack,
Name = "Pack",
Type = ItemType.Container,
ContainerId = player,
});
repo.ReplaceContents(
player,
[new ContainerContentEntry(pack, ContainerType: 1u)]);
ClientObject loose = MakeItem(100u, "Loose");
loose.ContainerId = player;
repo.AddOrUpdate(loose);
ClientObject packed = MakeItem(101u, "Packed");
packed.ContainerId = pack;
repo.AddOrUpdate(packed);
ClientObject equipped = MakeItem(102u, "Equipped");
equipped.WielderId = player;
equipped.CurrentlyEquippedLocation = EquipMask.MeleeWeapon;
repo.AddOrUpdate(equipped);
ClientObject foreign = MakeItem(103u, "Foreign");
foreign.ContainerId = 0x6000_0001u;
repo.AddOrUpdate(foreign);
Assert.True(repo.IsOwnedByObject(player, player));
Assert.True(repo.IsOwnedByObject(loose.ObjectId, player));
Assert.True(repo.IsOwnedByObject(packed.ObjectId, player));
Assert.True(repo.IsOwnedByObject(equipped.ObjectId, player));
Assert.False(repo.IsOwnedByObject(foreign.ObjectId, player));
Assert.False(repo.IsOwnedByObject(packed.ObjectId, 0u));
}
[Fact]
public void StopViewingContents_RemovesOnlyTemporaryProjection()
{
@ -673,6 +716,96 @@ public sealed class ClientObjectTableTests
Assert.Equal(new[] { item }, table.GetContents(pack));
}
[Fact]
public void AuthoritativePickup_PlacementZeroInsertsAtRetailListHead()
{
var table = new ClientObjectTable();
const uint corpse = 0x40000001u;
const uint pack = 0x50000001u;
const uint existingA = 0xA01u;
const uint existingB = 0xA02u;
const uint firstLoot = 0xA03u;
const uint secondLoot = 0xA04u;
table.InitializeInventoryManifest(pack, new[]
{
new ContainerContentEntry(existingA, 0u),
new ContainerContentEntry(existingB, 0u),
});
table.ReplaceContents(corpse, new[] { firstLoot, secondLoot });
Assert.True(table.ApplyConfirmedServerMove(
firstLoot,
pack,
newWielderId: 0u,
newSlot: 0,
containerTypeHint: 0u));
Assert.Equal(
new[] { firstLoot, existingA, existingB },
table.GetContents(pack));
Assert.Equal(
new[] { 0, 1, 2 },
table.GetContents(pack).Select(id => table.Get(id)!.ContainerSlot));
Assert.True(table.ApplyConfirmedServerMove(
secondLoot,
pack,
newWielderId: 0u,
newSlot: 0,
containerTypeHint: 0u));
Assert.Equal(
new[] { secondLoot, firstLoot, existingA, existingB },
table.GetContents(pack));
Assert.Equal(
new[] { 0, 1, 2, 3 },
table.GetContents(pack).Select(id => table.Get(id)!.ContainerSlot));
Assert.Empty(table.GetContents(corpse));
}
[Fact]
public void AuthoritativeContainerMove_IndexesOnlyTheRetailContainerList()
{
var table = new ClientObjectTable();
const uint pack = 0x50000001u;
const uint looseA = 0xA11u;
const uint looseB = 0xA12u;
const uint bagA = 0xA21u;
const uint bagB = 0xA22u;
table.InitializeInventoryManifest(pack, new[]
{
new ContainerContentEntry(looseA, 0u),
new ContainerContentEntry(bagA, 1u),
new ContainerContentEntry(looseB, 0u),
});
table.AddOrUpdate(new ClientObject
{
ObjectId = bagB,
Type = ItemType.Container,
ItemsCapacity = 24,
});
Assert.True(table.ApplyConfirmedServerMove(
bagB,
pack,
newWielderId: 0u,
newSlot: 0,
containerTypeHint: 1u));
uint[] loose = table.GetContents(pack)
.Where(id => table.Get(id)!.ContainerTypeHint == 0u
&& (table.Get(id)!.Type & ItemType.Container) == 0)
.ToArray();
uint[] bags = table.GetContents(pack)
.Where(id => table.Get(id)!.ContainerTypeHint != 0u
|| (table.Get(id)!.Type & ItemType.Container) != 0)
.ToArray();
Assert.Equal(new[] { looseA, looseB }, loose);
Assert.Equal(new[] { bagB, bagA }, bags);
Assert.Equal(0, table.Get(bagB)!.ContainerSlot);
Assert.Equal(1, table.Get(bagA)!.ContainerSlot);
}
[Fact]
public void ProjectionEvictionNotification_ReentrantMoveSeesCommittedState()
{