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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue