fix(interaction): restore retail loot placement and world-drop projection
This commit is contained in:
parent
4d095be286
commit
921712f412
24 changed files with 1581 additions and 34 deletions
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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))]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue