The first commit converted the four members the issue named and left the other sites alone, reasoning that none had been observed failing. A 20-run complete-solution baseline disproved that within minutes: run 2 LiveEntityRuntimeTests.AnimationView_HotSpatialTraversal… run 14 StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization… run 18 StaticRenderProjectionJournalTests.ActiveAnimatedSynchronization… run 19 CurrentRenderSceneOracleTests.SurfaceOverrideFingerprint… Both new names are the same shape as the four — one warm call, then a thousand-iteration loop inside the measured window — and neither had been recorded anywhere. "Not observed failing" only ever meant "not yet observed", and leaving known-shape sites in place would have guaranteed the acceptance gate failed. Run 19 is the sharper lesson: the issue named `SurfaceOverrideFingerprint_DictionaryHotPathAllocatesNothing`, and the first commit converted a *different* test in that same file, so the actually-named member was still on the old shape. Matching by file was not matching by test. Every strict-zero site in the assembly is now on the probe — ten tests. Two came out stricter rather than merely steadier: `StaticRenderProjectionJournalTests` was measuring a synchronise whose journal does **not** coalesce. Repeating it grew the journal by 1,000 entries per call — 192,000 by the end of a probe run — so the steady state the test claimed to measure did not exist and the single-call window had been hiding it. Its step is now the whole frame cycle, synchronise *and* drain, which puts `DrainTo` inside the measured window for the first time and asserts the journal ends empty. `RetailInboundEventDispatcherTests` asserted a hard-coded 1,001 callbacks. It now counts its own dispatches and pins the callback count against that, so the assertion still proves the fast path ran the callback every time without being coupled to a loop bound that no longer exists. Left alone deliberately: the four sites asserting a tolerance rather than zero — `CellViewDedupTests` and `PortalProjectionTests`. Their ceilings already absorb this noise and none has flaked; changing a bound in either direction is a separate decision from fixing a measurement. Worth noting that `PortalProjectionTests`' ceiling exists explicitly to tolerate "a tiered-JIT/ArrayPool bookkeeping transition ... to the first measured batch", which is exactly what the probe removes, so it could probably be tightened to zero now — recorded in the issue rather than done here. Solution build 0 warnings / 0 errors; App suite 3,941 passed / 3 skipped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1401 lines
54 KiB
C#
1401 lines
54 KiB
C#
using System.Collections;
|
|
using System.Numerics;
|
|
using System.Reflection;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Vfx;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.App.World;
|
|
using AcDream.Content;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Enums;
|
|
using AcDream.Runtime.Entities;
|
|
using DatReaderWriter.Types;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public sealed class EquippedChildProjectionWithdrawalTests
|
|
{
|
|
[Fact]
|
|
public void WithdrawalFailure_DoesNotPublishAttachedProjectionRemoval()
|
|
{
|
|
bool published = false;
|
|
|
|
ExactProjectionWithdrawalOutcome outcome =
|
|
EquippedChildRenderController.WithdrawAttachedProjection(
|
|
ChildRecord(),
|
|
positionAuthorityVersion: 1,
|
|
projectionMutationVersion: 2,
|
|
(_, _, _) => new ExactProjectionWithdrawalOutcome(
|
|
ExactProjectionWithdrawalDisposition.Pending,
|
|
new InvalidOperationException("injected component cleanup failure")),
|
|
() =>
|
|
{
|
|
published = true;
|
|
return true;
|
|
});
|
|
|
|
Assert.IsType<InvalidOperationException>(outcome.Failure);
|
|
Assert.Equal(ExactProjectionWithdrawalDisposition.Pending, outcome.Disposition);
|
|
Assert.False(published);
|
|
}
|
|
|
|
[Fact]
|
|
public void ExactIncarnationRejection_DoesNotPublishAttachedProjectionRemoval()
|
|
{
|
|
bool published = false;
|
|
|
|
ExactProjectionWithdrawalOutcome outcome = EquippedChildRenderController.WithdrawAttachedProjection(
|
|
ChildRecord(),
|
|
positionAuthorityVersion: 1,
|
|
projectionMutationVersion: 2,
|
|
(_, _, _) => new ExactProjectionWithdrawalOutcome(
|
|
ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null),
|
|
() =>
|
|
{
|
|
published = true;
|
|
return true;
|
|
});
|
|
|
|
Assert.True(published);
|
|
Assert.Equal(ExactProjectionWithdrawalDisposition.Superseded, outcome.Disposition);
|
|
}
|
|
|
|
[Fact]
|
|
public void SuccessfulWithdrawal_PublishesLocalProjectionRemovalOnce()
|
|
{
|
|
int publications = 0;
|
|
|
|
ExactProjectionWithdrawalOutcome outcome = EquippedChildRenderController.WithdrawAttachedProjection(
|
|
ChildRecord(),
|
|
positionAuthorityVersion: 1,
|
|
projectionMutationVersion: 2,
|
|
(_, _, _) => new ExactProjectionWithdrawalOutcome(
|
|
ExactProjectionWithdrawalDisposition.Completed,
|
|
Failure: null),
|
|
() =>
|
|
{
|
|
publications++;
|
|
return true;
|
|
});
|
|
|
|
Assert.Equal(ExactProjectionWithdrawalDisposition.Completed, outcome.Disposition);
|
|
Assert.Equal(1, publications);
|
|
}
|
|
|
|
[Fact]
|
|
public void LogicalReplacement_RemovesExactAttachedMapWithoutWithdrawingReplacement()
|
|
{
|
|
int withdrawals = 0;
|
|
using var fixture = new ControllerFixture(
|
|
(_, _, _) =>
|
|
{
|
|
withdrawals++;
|
|
return new(
|
|
ExactProjectionWithdrawalDisposition.Completed,
|
|
Failure: null);
|
|
});
|
|
LiveEntityRecord parent = fixture.Spawn(0x70000200u, generation: 1);
|
|
LiveEntityRecord oldChild = fixture.Spawn(
|
|
0x70000201u,
|
|
generation: 1,
|
|
LiveEntityProjectionKind.Attached);
|
|
fixture.InstallAttached(parent, oldChild);
|
|
|
|
LiveEntityRecord replacement = fixture.Live.RegisterAndMaterializeProjection(
|
|
ControllerFixture.SpawnData(0x70000201u, generation: 2));
|
|
|
|
Assert.Empty(fixture.Controller.AttachedEntityIds);
|
|
Assert.True(fixture.Live.TryGetRecord(0x70000201u, out LiveEntityRecord current));
|
|
Assert.Same(replacement, current);
|
|
Assert.Equal(0, withdrawals);
|
|
}
|
|
|
|
[Fact]
|
|
public void LogicalDelete_NotificationFailureLeavesRetryableTombstoneButNoAttachedLeak()
|
|
{
|
|
using var fixture = new ControllerFixture((_, _, _) =>
|
|
new(ExactProjectionWithdrawalDisposition.Completed, Failure: null));
|
|
LiveEntityRecord parent = fixture.Spawn(0x70000210u, generation: 1);
|
|
LiveEntityRecord child = fixture.Spawn(
|
|
0x70000211u,
|
|
generation: 1,
|
|
LiveEntityProjectionKind.Attached);
|
|
fixture.InstallAttached(parent, child);
|
|
fixture.Controller.ProjectionRemoved += Throw;
|
|
|
|
Assert.Throws<AggregateException>(() => fixture.Live.UnregisterLiveEntity(
|
|
new DeleteObject.Parsed(0x70000211u, InstanceSequence: 1),
|
|
isLocalPlayer: false));
|
|
Assert.Empty(fixture.Controller.AttachedEntityIds);
|
|
|
|
fixture.Controller.ProjectionRemoved -= Throw;
|
|
Assert.Equal(1, fixture.Live.RetryPendingTeardowns());
|
|
|
|
static void Throw(LiveEntityRecord _) =>
|
|
throw new InvalidOperationException("injected projection observer failure");
|
|
}
|
|
|
|
[Fact]
|
|
public void PostCommitWithdrawalFailure_RetiresCapturedAttachedMap()
|
|
{
|
|
ControllerFixture? fixture = null;
|
|
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
|
|
{
|
|
bool committed = fixture!.Live.WithdrawLiveEntityProjection(
|
|
record,
|
|
positionVersion,
|
|
projectionVersion);
|
|
Assert.True(committed);
|
|
return new(
|
|
ExactProjectionWithdrawalDisposition.Completed,
|
|
new InvalidOperationException("observer failed after commit"));
|
|
});
|
|
using (fixture)
|
|
{
|
|
LiveEntityRecord parent = fixture.Spawn(0x70000220u, generation: 1);
|
|
LiveEntityRecord child = fixture.Spawn(
|
|
0x70000221u,
|
|
generation: 1,
|
|
LiveEntityProjectionKind.Attached);
|
|
fixture.InstallAttached(parent, child);
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
fixture.Controller.OnChildBecameUnparented(0x70000221u));
|
|
Assert.Empty(fixture.Controller.AttachedEntityIds);
|
|
Assert.False(child.IsSpatiallyProjected);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void PendingTopLevelWithdrawal_RetryRunsAcceptedContinuationOnce()
|
|
{
|
|
int attempts = 0;
|
|
ControllerFixture? fixture = null;
|
|
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
|
|
{
|
|
attempts++;
|
|
if (attempts == 1)
|
|
{
|
|
return new(
|
|
ExactProjectionWithdrawalDisposition.Pending,
|
|
new InvalidOperationException("component cleanup failed"));
|
|
}
|
|
bool committed = fixture!.Live.WithdrawLiveEntityProjection(
|
|
record,
|
|
positionVersion,
|
|
projectionVersion);
|
|
return new(
|
|
committed
|
|
? ExactProjectionWithdrawalDisposition.Completed
|
|
: ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
});
|
|
using (fixture)
|
|
{
|
|
LiveEntityRecord record = fixture.Spawn(0x70000230u, generation: 1);
|
|
int continuations = 0;
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
fixture.Controller.OnChildBecameUnparented(
|
|
0x70000230u,
|
|
() =>
|
|
{
|
|
continuations++;
|
|
Assert.True(fixture.Live.RebucketLiveEntity(
|
|
0x70000230u,
|
|
0x01010001u));
|
|
}));
|
|
|
|
fixture.Controller.Tick();
|
|
|
|
Assert.Equal(2, attempts);
|
|
Assert.Equal(1, continuations);
|
|
Assert.True(record.IsSpatiallyProjected);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void PendingUnparent_NewerPositionSupersedesRecoveryContinuation()
|
|
{
|
|
using var fixture = new ControllerFixture((_, _, _) =>
|
|
new(
|
|
ExactProjectionWithdrawalDisposition.Pending,
|
|
new InvalidOperationException("component cleanup failed")));
|
|
LiveEntityRecord record = fixture.Spawn(0x70000240u, generation: 1);
|
|
int continuations = 0;
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
fixture.Controller.OnChildBecameUnparented(
|
|
0x70000240u,
|
|
() => continuations++));
|
|
|
|
var newer = new WorldSession.EntityPositionUpdate(
|
|
0x70000240u,
|
|
new CreateObject.ServerPosition(
|
|
0x01010001u, 4f, 5f, 6f, 1f, 0f, 0f, 0f),
|
|
Velocity: null,
|
|
PlacementId: null,
|
|
IsGrounded: true,
|
|
InstanceSequence: 1,
|
|
PositionSequence: 2,
|
|
TeleportSequence: 0,
|
|
ForcePositionSequence: 0);
|
|
Assert.True(fixture.Live.TryApplyPosition(
|
|
newer,
|
|
isLocalPlayer: false,
|
|
forcePositionRotation: null,
|
|
currentLocalVelocity: null,
|
|
out _,
|
|
out _,
|
|
out _));
|
|
|
|
fixture.Controller.Tick();
|
|
|
|
Assert.Equal(0, continuations);
|
|
Assert.True(record.IsSpatiallyProjected);
|
|
}
|
|
|
|
[Fact]
|
|
public void PostNetworkReconcile_SkipsStableTree_AndUpdatesChangedBranchParentFirst()
|
|
{
|
|
using var fixture = new ControllerFixture((_, _, _) =>
|
|
new(
|
|
ExactProjectionWithdrawalDisposition.Completed,
|
|
Failure: null));
|
|
LiveEntityRecord root = fixture.Spawn(0x70000241u, generation: 1);
|
|
LiveEntityRecord child = fixture.Spawn(
|
|
0x70000242u,
|
|
generation: 1,
|
|
LiveEntityProjectionKind.Attached);
|
|
LiveEntityRecord grandchild = fixture.Spawn(
|
|
0x70000243u,
|
|
generation: 1,
|
|
LiveEntityProjectionKind.Attached);
|
|
fixture.InstallAttached(root, child);
|
|
fixture.InstallAttached(child, grandchild);
|
|
fixture.Poses.Publish(root.WorldEntity!, Array.Empty<Matrix4x4>());
|
|
|
|
fixture.Controller.Tick();
|
|
Assert.Equal(2, fixture.Controller.LastFullPoseCompositionVisits);
|
|
|
|
fixture.Controller.ReconcileSpatialMutations();
|
|
Assert.Equal(0, fixture.Controller.LastReconcilePoseCompositionVisits);
|
|
|
|
root.WorldEntity!.SetPosition(new Vector3(7f, 8f, 9f));
|
|
Assert.True(fixture.Poses.UpdateRoot(root.WorldEntity));
|
|
fixture.Controller.ReconcileSpatialMutations();
|
|
|
|
Assert.Equal(2, fixture.Controller.LastReconcilePoseCompositionVisits);
|
|
Assert.Equal(root.WorldEntity.Position, child.WorldEntity!.Position);
|
|
Assert.Equal(child.WorldEntity.Position, grandchild.WorldEntity!.Position);
|
|
}
|
|
|
|
[Fact]
|
|
public void StablePostNetworkReconcile_AllocatesNoTransitionSnapshots()
|
|
{
|
|
using var fixture = new ControllerFixture((_, _, _) =>
|
|
new(
|
|
ExactProjectionWithdrawalDisposition.Completed,
|
|
Failure: null));
|
|
LiveEntityRecord root = fixture.Spawn(0x70000244u, generation: 1);
|
|
LiveEntityRecord child = fixture.Spawn(
|
|
0x70000245u,
|
|
generation: 1,
|
|
LiveEntityProjectionKind.Attached);
|
|
fixture.InstallAttached(root, child);
|
|
fixture.Poses.Publish(root.WorldEntity!, Array.Empty<Matrix4x4>());
|
|
fixture.Controller.Tick();
|
|
fixture.Controller.ReconcileSpatialMutations();
|
|
|
|
// #250: the measured window was a 1,000-iteration loop written inline.
|
|
ZeroAllocationProbe.AssertAllocatesNothing(
|
|
"LiveEntityController.ReconcileSpatialMutations",
|
|
() => fixture.Controller.ReconcileSpatialMutations());
|
|
|
|
Assert.Equal(0, fixture.Controller.LastReconcilePoseCompositionVisits);
|
|
}
|
|
|
|
[Fact]
|
|
public void AcceptedValidParent_WithdrawsWorldProjectionBeforePosePrerequisitesExist()
|
|
{
|
|
ControllerFixture? fixture = null;
|
|
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
|
|
{
|
|
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
|
|
record,
|
|
positionVersion,
|
|
projectionVersion);
|
|
return new(
|
|
completed
|
|
? ExactProjectionWithdrawalDisposition.Completed
|
|
: ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
});
|
|
using (fixture)
|
|
{
|
|
fixture.Spawn(0x70000250u, generation: 1);
|
|
LiveEntityRecord child = fixture.Spawn(0x70000251u, generation: 1);
|
|
|
|
fixture.Controller.OnParentEvent(new ParentEvent.Parsed(
|
|
ParentGuid: 0x70000250u,
|
|
ChildGuid: 0x70000251u,
|
|
ParentLocation: 0,
|
|
PlacementId: 0,
|
|
ParentInstanceSequence: 1,
|
|
ChildPositionSequence: 2));
|
|
|
|
Assert.False(child.IsSpatiallyProjected);
|
|
Assert.True(fixture.Live.ParentAttachments.TryGetProjection(
|
|
0x70000251u,
|
|
out _));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void InvalidParent_ConsumesPositionTimestampButPreservesWorldProjection()
|
|
{
|
|
ControllerFixture? fixture = null;
|
|
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
|
|
{
|
|
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
|
|
record,
|
|
positionVersion,
|
|
projectionVersion);
|
|
return new(
|
|
completed
|
|
? ExactProjectionWithdrawalDisposition.Completed
|
|
: ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
});
|
|
using (fixture)
|
|
{
|
|
fixture.Spawn(0x70000252u, generation: 1);
|
|
LiveEntityRecord child = fixture.Spawn(0x70000253u, generation: 1);
|
|
|
|
fixture.Controller.OnParentEvent(new ParentEvent.Parsed(
|
|
ParentGuid: 0x70000252u,
|
|
ChildGuid: 0x70000253u,
|
|
ParentLocation: 1,
|
|
PlacementId: 0,
|
|
ParentInstanceSequence: 1,
|
|
ChildPositionSequence: 2));
|
|
|
|
Assert.True(child.IsSpatiallyProjected);
|
|
Assert.NotEqual(0u, child.FullCellId);
|
|
Assert.True(fixture.Live.TryGetSnapshot(
|
|
child.ServerGuid,
|
|
out WorldSession.EntitySpawn snapshot));
|
|
Assert.NotNull(snapshot.Position);
|
|
Assert.Null(snapshot.ParentGuid);
|
|
Assert.Equal((ushort)2, snapshot.PositionSequence);
|
|
Assert.False(fixture.Live.ParentAttachments.TryGetProjection(
|
|
child.ServerGuid,
|
|
out _));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void QueuedValidThenInvalidParent_LeavesValidParentCommitted()
|
|
{
|
|
ControllerFixture? fixture = null;
|
|
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
|
|
{
|
|
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
|
|
record,
|
|
positionVersion,
|
|
projectionVersion);
|
|
return new(
|
|
completed
|
|
? ExactProjectionWithdrawalDisposition.Completed
|
|
: ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
});
|
|
using (fixture)
|
|
{
|
|
LiveEntityRecord validParent = fixture.Spawn(0x70000259u, generation: 1);
|
|
LiveEntityRecord invalidParent = fixture.Spawn(0x7000025Au, generation: 1);
|
|
const uint childGuid = 0x7000025Bu;
|
|
fixture.Controller.OnParentEvent(new ParentEvent.Parsed(
|
|
validParent.ServerGuid, childGuid, 0, 0, 1, 1));
|
|
fixture.Controller.OnParentEvent(new ParentEvent.Parsed(
|
|
invalidParent.ServerGuid, childGuid, 1, 0, 1, 2));
|
|
|
|
RuntimeEntityRecord childIdentity = fixture.RegisterOnly(
|
|
childGuid,
|
|
generation: 1,
|
|
hasPosition: true);
|
|
LiveEntityRecord child = fixture.Materialize(childIdentity);
|
|
Assert.True(fixture.Live.TryGetSnapshot(
|
|
childGuid,
|
|
out WorldSession.EntitySpawn childSpawn));
|
|
fixture.Controller.OnSpawn(childSpawn);
|
|
|
|
Assert.True(fixture.Live.TryGetSnapshot(childGuid, out childSpawn));
|
|
Assert.Equal(validParent.ServerGuid, childSpawn.ParentGuid);
|
|
Assert.Null(childSpawn.Position);
|
|
Assert.Equal((ushort)2, childSpawn.PositionSequence);
|
|
var valid = new ParentAttachmentRelation(
|
|
validParent.ServerGuid, childGuid, 0, 0, 1, 1);
|
|
Assert.True(fixture.Live.ParentAttachments.IsCommitted(valid));
|
|
Assert.False(fixture.Live.ParentAttachments.TryGetStagedProjection(
|
|
childGuid,
|
|
out _));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void NoPositionCreateParent_CommitsAfterParentPartArrayValidation()
|
|
{
|
|
using var fixture = new ControllerFixture((_, _, _) =>
|
|
new ExactProjectionWithdrawalOutcome(
|
|
ExactProjectionWithdrawalDisposition.Completed,
|
|
Failure: null));
|
|
LiveEntityRecord parent = fixture.Spawn(0x70000254u, generation: 1);
|
|
RuntimeEntityRecord child = fixture.RegisterOnly(
|
|
0x70000255u,
|
|
generation: 1,
|
|
hasPosition: false);
|
|
var update = new CreateParentUpdate(
|
|
child.ServerGuid,
|
|
parent.ServerGuid,
|
|
ParentLocation: 0,
|
|
PlacementId: 0,
|
|
ChildInstanceSequence: 1,
|
|
ChildPositionSequence: 1);
|
|
|
|
Assert.True(fixture.Live.TryApplyCreateParent(update, out _));
|
|
fixture.Controller.OnCreateParentAccepted(update);
|
|
|
|
Assert.True(fixture.Live.TryGetSnapshot(
|
|
child.ServerGuid,
|
|
out WorldSession.EntitySpawn snapshot));
|
|
Assert.Equal(parent.ServerGuid, snapshot.ParentGuid);
|
|
Assert.Null(snapshot.Position);
|
|
Assert.False(fixture.Live.TryGetRecord(child.ServerGuid, out _));
|
|
var relation = new ParentAttachmentRelation(
|
|
parent.ServerGuid,
|
|
child.ServerGuid,
|
|
0,
|
|
0,
|
|
0,
|
|
1);
|
|
Assert.True(fixture.Live.ParentAttachments.IsCommitted(relation));
|
|
|
|
fixture.Poses.Publish(
|
|
parent.WorldEntity!,
|
|
Array.Empty<Matrix4x4>());
|
|
fixture.Controller.OnPosePublished(parent.ServerGuid);
|
|
|
|
Assert.True(fixture.Live.TryGetRecord(
|
|
child.ServerGuid,
|
|
out LiveEntityRecord childProjection));
|
|
Assert.NotNull(childProjection.WorldEntity);
|
|
Assert.True(childProjection.IsSpatiallyProjected);
|
|
Assert.Equal(
|
|
LiveEntityProjectionKind.Attached,
|
|
childProjection.ProjectionKind);
|
|
}
|
|
|
|
[Fact]
|
|
public void SpawnParentWithoutAnimationFrame_UsesRetailPlacementZero()
|
|
{
|
|
using var fixture = new ControllerFixture((_, _, _) =>
|
|
new ExactProjectionWithdrawalOutcome(
|
|
ExactProjectionWithdrawalDisposition.Completed,
|
|
Failure: null));
|
|
LiveEntityRecord parent = fixture.Spawn(0x70000270u, generation: 1);
|
|
WorldSession.EntitySpawn childSpawn = ControllerFixture.SpawnData(
|
|
0x70000271u,
|
|
generation: 1) with
|
|
{
|
|
Position = null,
|
|
ParentGuid = parent.ServerGuid,
|
|
ParentLocation = 0,
|
|
PlacementId = null,
|
|
PositionSequence = 0,
|
|
};
|
|
fixture.Live.RegisterLiveEntity(childSpawn);
|
|
fixture.Controller.OnSpawn(childSpawn);
|
|
|
|
var expected = new ParentAttachmentRelation(
|
|
parent.ServerGuid,
|
|
childSpawn.Guid,
|
|
ParentLocation: 0,
|
|
PlacementId: 0,
|
|
ParentInstanceSequence: 0,
|
|
ChildPositionSequence: 0);
|
|
Assert.True(fixture.Live.ParentAttachments.IsCommitted(expected));
|
|
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
|
|
fixture.Controller.OnPosePublished(parent.ServerGuid);
|
|
Assert.True(fixture.Live.TryGetRecord(
|
|
childSpawn.Guid,
|
|
out LiveEntityRecord child));
|
|
Assert.Equal(LiveEntityProjectionKind.Attached, child.ProjectionKind);
|
|
Assert.NotNull(child.WorldEntity);
|
|
}
|
|
|
|
[Fact]
|
|
public void ObjDesc_ReprojectsAttachedChildWithoutWorldPositionOrIdentityChange()
|
|
{
|
|
ControllerFixture? fixture = null;
|
|
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
|
|
{
|
|
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
|
|
record,
|
|
positionVersion,
|
|
projectionVersion);
|
|
return new(
|
|
completed
|
|
? ExactProjectionWithdrawalDisposition.Completed
|
|
: ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
});
|
|
using (fixture)
|
|
{
|
|
LiveEntityRecord parent = fixture.Spawn(0x70000272u, generation: 1);
|
|
WorldSession.EntitySpawn childSpawn = ControllerFixture.SpawnData(
|
|
0x70000273u,
|
|
generation: 1) with
|
|
{
|
|
Position = null,
|
|
ParentGuid = parent.ServerGuid,
|
|
ParentLocation = 0,
|
|
PlacementId = 0,
|
|
PositionSequence = 0,
|
|
};
|
|
fixture.Live.RegisterLiveEntity(childSpawn);
|
|
fixture.Controller.OnSpawn(childSpawn);
|
|
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
|
|
fixture.Controller.OnPosePublished(parent.ServerGuid);
|
|
Assert.True(fixture.Live.TryGetRecord(
|
|
childSpawn.Guid,
|
|
out LiveEntityRecord child));
|
|
WorldEntity entity = child.WorldEntity!;
|
|
Assert.Null(entity.PaletteOverride);
|
|
WorldSession.EntitySpawn grandchildSpawn = ControllerFixture.SpawnData(
|
|
0x70000274u,
|
|
generation: 1) with
|
|
{
|
|
Position = null,
|
|
ParentGuid = child.ServerGuid,
|
|
ParentLocation = 0,
|
|
PlacementId = 0,
|
|
PositionSequence = 0,
|
|
};
|
|
fixture.Live.RegisterLiveEntity(grandchildSpawn);
|
|
fixture.Controller.OnSpawn(grandchildSpawn);
|
|
Assert.True(fixture.Live.TryGetRecord(
|
|
grandchildSpawn.Guid,
|
|
out LiveEntityRecord grandchild));
|
|
WorldEntity grandchildEntity = grandchild.WorldEntity!;
|
|
Assert.Equal(
|
|
LiveEntityProjectionKind.Attached,
|
|
grandchild.ProjectionKind);
|
|
|
|
Assert.True(fixture.Live.TryApplyObjDesc(
|
|
new ObjDescEvent.Parsed(
|
|
child.ServerGuid,
|
|
new CreateObject.ModelData(
|
|
BasePaletteId: 0x04000022u,
|
|
[new CreateObject.SubPaletteSwap(
|
|
0x0F000033u,
|
|
Offset: 4,
|
|
Length: 8)],
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.AnimPartChange>()),
|
|
InstanceSequence: 1,
|
|
ObjDescSequence: 1),
|
|
out _));
|
|
Assert.Null(child.Snapshot.Position);
|
|
|
|
Assert.True(fixture.Controller.TryApplyAttachedAppearance(
|
|
child,
|
|
child.ObjDescAuthorityVersion));
|
|
|
|
Assert.Same(entity, child.WorldEntity);
|
|
Assert.True(child.IsSpatiallyProjected);
|
|
Assert.Equal(LiveEntityProjectionKind.Attached, child.ProjectionKind);
|
|
Assert.Equal((uint)0x04000022u, child.Snapshot.BasePaletteId);
|
|
Assert.NotNull(entity.PaletteOverride);
|
|
Assert.Equal(0x04000022u, entity.PaletteOverride!.BasePaletteId);
|
|
PaletteOverride.SubPaletteRange range =
|
|
Assert.Single(entity.PaletteOverride.SubPalettes);
|
|
Assert.Equal(0x0F000033u, range.SubPaletteId);
|
|
Assert.Equal((byte)4, range.Offset);
|
|
Assert.Equal((byte)8, range.Length);
|
|
Assert.Same(grandchildEntity, grandchild.WorldEntity);
|
|
Assert.True(grandchild.IsSpatiallyProjected);
|
|
Assert.Equal(
|
|
LiveEntityProjectionKind.Attached,
|
|
grandchild.ProjectionKind);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void ChildWithoutSetup_StillCommitsLogicalParenting()
|
|
{
|
|
ControllerFixture? fixture = null;
|
|
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
|
|
{
|
|
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
|
|
record,
|
|
positionVersion,
|
|
projectionVersion);
|
|
return new(
|
|
completed
|
|
? ExactProjectionWithdrawalDisposition.Completed
|
|
: ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
});
|
|
using (fixture)
|
|
{
|
|
LiveEntityRecord parent = fixture.Spawn(0x7000025Cu, generation: 1);
|
|
RuntimeEntityRecord childIdentity = fixture.RegisterOnly(
|
|
0x7000025Du,
|
|
generation: 1,
|
|
hasPosition: true,
|
|
hasSetup: false);
|
|
LiveEntityRecord child = fixture.Materialize(childIdentity);
|
|
child.HasPartArray = false;
|
|
var update = new ParentEvent.Parsed(
|
|
parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1);
|
|
|
|
fixture.Controller.OnParentEvent(update);
|
|
|
|
Assert.True(fixture.Live.TryGetSnapshot(
|
|
child.ServerGuid,
|
|
out WorldSession.EntitySpawn snapshot));
|
|
Assert.Equal(parent.ServerGuid, snapshot.ParentGuid);
|
|
Assert.Null(snapshot.Position);
|
|
Assert.False(child.IsSpatiallyProjected);
|
|
Assert.True(fixture.Live.ParentAttachments.IsCommitted(new(
|
|
parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1)));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void EmbeddedSameGenerationCreateParent_UsesStagedRoute()
|
|
{
|
|
ControllerFixture? fixture = null;
|
|
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
|
|
{
|
|
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
|
|
record,
|
|
positionVersion,
|
|
projectionVersion);
|
|
return new(
|
|
completed
|
|
? ExactProjectionWithdrawalDisposition.Completed
|
|
: ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
});
|
|
using (fixture)
|
|
{
|
|
LiveEntityRecord parent = fixture.Spawn(0x7000025Eu, generation: 1);
|
|
LiveEntityRecord child = fixture.Spawn(0x7000025Fu, generation: 1);
|
|
PhysicsTimestamps timestamps = new(
|
|
Position: 1,
|
|
Movement: 0,
|
|
State: 0,
|
|
Vector: 0,
|
|
Teleport: 0,
|
|
ServerControlledMove: 0,
|
|
ForcePosition: 0,
|
|
ObjDesc: 0,
|
|
Instance: 1);
|
|
PhysicsSpawnData physics = new(
|
|
RawState: 0x408u,
|
|
Position: null,
|
|
Movement: null,
|
|
AnimationFrame: 0,
|
|
SetupTableId: 0x02000001u,
|
|
MotionTableId: null,
|
|
SoundTableId: null,
|
|
PhysicsScriptTableId: null,
|
|
Parent: new PhysicsAttachment(parent.ServerGuid, 0),
|
|
Children: null,
|
|
Scale: null,
|
|
Friction: null,
|
|
Elasticity: null,
|
|
Translucency: null,
|
|
Velocity: null,
|
|
Acceleration: null,
|
|
AngularVelocity: null,
|
|
DefaultScriptType: null,
|
|
DefaultScriptIntensity: null,
|
|
Timestamps: timestamps);
|
|
WorldSession.EntitySpawn incoming = ControllerFixture.SpawnData(
|
|
child.ServerGuid,
|
|
generation: 1) with
|
|
{
|
|
Position = null,
|
|
PositionSequence = 1,
|
|
PlacementId = 0,
|
|
Physics = physics,
|
|
};
|
|
|
|
LiveEntityRegistrationResult refresh = fixture.Live.RegisterLiveEntity(incoming);
|
|
CreateParentUpdate update = Assert.IsType<CreateParentUpdate>(
|
|
refresh.Inbound.SameGenerationEvents!.Value.Parent);
|
|
Assert.True(fixture.Live.TryApplyCreateParent(update, out _));
|
|
fixture.Controller.OnCreateParentAccepted(update);
|
|
|
|
Assert.True(fixture.Live.TryGetSnapshot(
|
|
child.ServerGuid,
|
|
out WorldSession.EntitySpawn snapshot));
|
|
Assert.Equal(parent.ServerGuid, snapshot.ParentGuid);
|
|
Assert.Null(snapshot.Position);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void NewWaitingParent_DoesNotDisplaceCommittedRecovery()
|
|
{
|
|
ControllerFixture? fixture = null;
|
|
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
|
|
{
|
|
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
|
|
record,
|
|
positionVersion,
|
|
projectionVersion);
|
|
return new(
|
|
completed
|
|
? ExactProjectionWithdrawalDisposition.Completed
|
|
: ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
});
|
|
using (fixture)
|
|
{
|
|
LiveEntityRecord oldParent = fixture.Spawn(0x70000256u, generation: 1);
|
|
LiveEntityRecord child = fixture.Spawn(
|
|
0x70000257u,
|
|
generation: 1,
|
|
LiveEntityProjectionKind.Attached);
|
|
RuntimeEntityRecord waitingParent = fixture.RegisterOnly(
|
|
0x70000258u,
|
|
generation: 1,
|
|
hasPosition: true);
|
|
var oldRelation = new ParentAttachmentRelation(
|
|
oldParent.ServerGuid,
|
|
child.ServerGuid,
|
|
0,
|
|
0,
|
|
1,
|
|
1);
|
|
fixture.Live.ParentAttachments.AcceptCreateObjectRelation(oldRelation);
|
|
Assert.True(fixture.Live.TryApplyParent(new ParentEvent.Parsed(
|
|
oldRelation.ParentGuid,
|
|
oldRelation.ChildGuid,
|
|
oldRelation.ParentLocation,
|
|
oldRelation.PlacementId,
|
|
oldRelation.ParentInstanceSequence,
|
|
oldRelation.ChildPositionSequence), out _));
|
|
Assert.True(fixture.Live.CommitStagedParent(oldRelation, out _));
|
|
Assert.True(fixture.Live.ParentAttachments.CommitProjection(oldRelation));
|
|
fixture.Live.ParentAttachments.MarkProjected(
|
|
oldRelation,
|
|
ParentProjectionCandidateKind.Recovery);
|
|
fixture.InstallAttached(oldParent, child);
|
|
|
|
var newer = new ParentEvent.Parsed(
|
|
waitingParent.ServerGuid,
|
|
child.ServerGuid,
|
|
ParentLocation: 0,
|
|
PlacementId: 0,
|
|
ParentInstanceSequence: 1,
|
|
ChildPositionSequence: 2);
|
|
fixture.Controller.OnParentEvent(newer);
|
|
Assert.True(fixture.Live.ParentAttachments.TryGetStagedProjection(
|
|
child.ServerGuid,
|
|
out ParentAttachmentRelation staged));
|
|
Assert.Equal(waitingParent.ServerGuid, staged.ParentGuid);
|
|
|
|
fixture.Controller.Tick();
|
|
|
|
Assert.Empty(fixture.Controller.AttachedEntityIds);
|
|
Assert.True(fixture.Live.ParentAttachments.TryGetStagedProjection(
|
|
child.ServerGuid,
|
|
out staged));
|
|
Assert.Equal(waitingParent.ServerGuid, staged.ParentGuid);
|
|
Assert.True(fixture.Live.ParentAttachments.TryGetRecoveryProjection(
|
|
child.ServerGuid,
|
|
out ParentAttachmentRelation recovery));
|
|
Assert.Equal(oldParent.ServerGuid, recovery.ParentGuid);
|
|
|
|
fixture.Materialize(waitingParent);
|
|
fixture.Controller.OnWorldEntityRegistered(waitingParent.ServerGuid);
|
|
|
|
Assert.True(fixture.Live.ParentAttachments.IsCommitted(staged));
|
|
Assert.True(fixture.Live.ParentAttachments.TryGetRecoveryProjection(
|
|
child.ServerGuid,
|
|
out ParentAttachmentRelation committedRecovery));
|
|
Assert.Equal(staged, committedRecovery);
|
|
Assert.True(fixture.Live.TryGetSnapshot(
|
|
child.ServerGuid,
|
|
out WorldSession.EntitySpawn snapshot));
|
|
Assert.Equal(waitingParent.ServerGuid, snapshot.ParentGuid);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void OrdinaryRemoval_PendingFailureRetriesExactCaptureOnTick()
|
|
{
|
|
int attempts = 0;
|
|
ControllerFixture? fixture = null;
|
|
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
|
|
{
|
|
attempts++;
|
|
if (attempts == 1)
|
|
{
|
|
return new(
|
|
ExactProjectionWithdrawalDisposition.Pending,
|
|
new InvalidOperationException("ordinary cleanup failed"));
|
|
}
|
|
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
|
|
record,
|
|
positionVersion,
|
|
projectionVersion);
|
|
return new(
|
|
completed
|
|
? ExactProjectionWithdrawalDisposition.Completed
|
|
: ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
});
|
|
using (fixture)
|
|
{
|
|
LiveEntityRecord parent = fixture.Spawn(0x70000260u, generation: 1);
|
|
LiveEntityRecord child = fixture.Spawn(
|
|
0x70000261u,
|
|
generation: 1,
|
|
LiveEntityProjectionKind.Attached);
|
|
fixture.InstallAttached(parent, child);
|
|
fixture.Live.ParentAttachments.AcceptCreateObjectRelation(new(
|
|
parent.ServerGuid,
|
|
child.ServerGuid,
|
|
ParentLocation: 0,
|
|
PlacementId: 0,
|
|
ParentInstanceSequence: 1,
|
|
ChildPositionSequence: 1));
|
|
|
|
TargetInvocationException error = Assert.Throws<TargetInvocationException>(() =>
|
|
fixture.InvokeOrdinaryRemoval(0x70000261u));
|
|
Assert.IsType<InvalidOperationException>(error.InnerException);
|
|
|
|
fixture.Controller.Tick();
|
|
|
|
Assert.Equal(2, attempts);
|
|
Assert.Empty(fixture.Controller.AttachedEntityIds);
|
|
Assert.False(child.IsSpatiallyProjected);
|
|
Assert.False(fixture.Live.ParentAttachments.TryGetProjection(
|
|
child.ServerGuid,
|
|
out _));
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void DetachedRemoval_PendingFailureRetriesAndPreservesRollbackHistory()
|
|
{
|
|
int attempts = 0;
|
|
ControllerFixture? fixture = null;
|
|
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
|
|
{
|
|
attempts++;
|
|
if (attempts == 1)
|
|
{
|
|
return new(
|
|
ExactProjectionWithdrawalDisposition.Pending,
|
|
new InvalidOperationException("detached cleanup failed"));
|
|
}
|
|
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
|
|
record,
|
|
positionVersion,
|
|
projectionVersion);
|
|
return new(
|
|
completed
|
|
? ExactProjectionWithdrawalDisposition.Completed
|
|
: ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
});
|
|
using (fixture)
|
|
{
|
|
LiveEntityRecord parent = fixture.Spawn(0x70000262u, generation: 1);
|
|
LiveEntityRecord child = fixture.Spawn(
|
|
0x70000263u,
|
|
generation: 1,
|
|
LiveEntityProjectionKind.Attached);
|
|
fixture.InstallAttached(parent, child);
|
|
var relation = new ParentAttachmentRelation(
|
|
parent.ServerGuid,
|
|
child.ServerGuid,
|
|
ParentLocation: 0,
|
|
PlacementId: 0,
|
|
ParentInstanceSequence: 1,
|
|
ChildPositionSequence: 1);
|
|
fixture.Live.ParentAttachments.AcceptCreateObjectRelation(relation);
|
|
Assert.True(fixture.Live.ParentAttachments.CommitProjection(relation));
|
|
fixture.Live.ParentAttachments.MarkProjected(
|
|
relation,
|
|
ParentProjectionCandidateKind.Recovery);
|
|
|
|
TargetInvocationException error = Assert.Throws<TargetInvocationException>(() =>
|
|
fixture.InvokeDetachedRemoval(child.ServerGuid));
|
|
Assert.IsType<InvalidOperationException>(error.InnerException);
|
|
|
|
fixture.Controller.Tick();
|
|
|
|
Assert.Equal(2, attempts);
|
|
Assert.Empty(fixture.Controller.AttachedEntityIds);
|
|
Assert.False(child.IsSpatiallyProjected);
|
|
Assert.True(fixture.Live.ParentAttachments.RestoreLastAccepted(
|
|
child.ServerGuid));
|
|
Assert.True(fixture.Live.ParentAttachments.TryGetRecoveryProjection(
|
|
child.ServerGuid,
|
|
out ParentAttachmentRelation restored));
|
|
Assert.Equal(relation, restored);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Unparent_WithdrawsCompleteAttachedSubtreeAndRecoversDescendants()
|
|
{
|
|
ControllerFixture? fixture = null;
|
|
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
|
|
{
|
|
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
|
|
record,
|
|
positionVersion,
|
|
projectionVersion);
|
|
return new(
|
|
completed
|
|
? ExactProjectionWithdrawalDisposition.Completed
|
|
: ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
});
|
|
using (fixture)
|
|
{
|
|
LiveEntityRecord parent = fixture.Spawn(0x70000264u, generation: 1);
|
|
LiveEntityRecord child = fixture.Spawn(
|
|
0x70000265u,
|
|
generation: 1,
|
|
LiveEntityProjectionKind.Attached);
|
|
LiveEntityRecord grandchild = fixture.Spawn(
|
|
0x70000266u,
|
|
generation: 1,
|
|
LiveEntityProjectionKind.Attached);
|
|
fixture.InstallAttached(parent, child);
|
|
fixture.InstallAttached(child, grandchild);
|
|
var childRelation = new ParentAttachmentRelation(
|
|
parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1);
|
|
var grandchildRelation = new ParentAttachmentRelation(
|
|
child.ServerGuid, grandchild.ServerGuid, 0, 0, 1, 1);
|
|
fixture.CommitRenderedRelation(childRelation);
|
|
fixture.CommitRenderedRelation(grandchildRelation);
|
|
|
|
ChildUnparentDisposition result =
|
|
fixture.Controller.OnChildBecameUnparented(child.ServerGuid);
|
|
|
|
Assert.Equal(ChildUnparentDisposition.Completed, result);
|
|
Assert.Empty(fixture.Controller.AttachedEntityIds);
|
|
Assert.False(child.IsSpatiallyProjected);
|
|
Assert.False(grandchild.IsSpatiallyProjected);
|
|
Assert.False(fixture.Live.ParentAttachments.RestoreLastAccepted(
|
|
child.ServerGuid));
|
|
Assert.True(fixture.Live.ParentAttachments.TryGetRecoveryProjection(
|
|
grandchild.ServerGuid,
|
|
out ParentAttachmentRelation recovered));
|
|
Assert.Equal(grandchildRelation, recovered);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void PendingDetachedRemoval_RollbackCancelsRetryAndKeepsProjection()
|
|
{
|
|
int attempts = 0;
|
|
using var fixture = new ControllerFixture((_, _, _) =>
|
|
{
|
|
attempts++;
|
|
return new(
|
|
ExactProjectionWithdrawalDisposition.Pending,
|
|
new InvalidOperationException("component withdrawal failed"));
|
|
});
|
|
LiveEntityRecord parent = fixture.Spawn(0x70000267u, generation: 1);
|
|
LiveEntityRecord child = fixture.Spawn(
|
|
0x70000268u,
|
|
generation: 1,
|
|
LiveEntityProjectionKind.Attached);
|
|
fixture.InstallAttached(parent, child);
|
|
fixture.CommitRenderedRelation(new(
|
|
parent.ServerGuid, child.ServerGuid, 0, 0, 1, 1));
|
|
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
|
|
fixture.Objects.AddOrUpdate(new ClientObject
|
|
{
|
|
ObjectId = child.ServerGuid,
|
|
WielderId = parent.ServerGuid,
|
|
CurrentlyEquippedLocation = EquipMask.MeleeWeapon,
|
|
});
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
fixture.Objects.MoveItemOptimistic(
|
|
child.ServerGuid,
|
|
newContainerId: 0x50000001u,
|
|
newSlot: 0));
|
|
Assert.True(fixture.Objects.RollbackMove(child.ServerGuid));
|
|
fixture.Controller.Tick();
|
|
|
|
Assert.Equal(1, attempts);
|
|
Assert.True(child.IsSpatiallyProjected);
|
|
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()
|
|
{
|
|
int attempts = 0;
|
|
ControllerFixture? fixture = null;
|
|
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
|
|
{
|
|
attempts++;
|
|
if (attempts == 1)
|
|
{
|
|
return new(
|
|
ExactProjectionWithdrawalDisposition.Pending,
|
|
new InvalidOperationException("orphan component cleanup failed"));
|
|
}
|
|
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
|
|
record,
|
|
positionVersion,
|
|
projectionVersion);
|
|
return new(
|
|
completed
|
|
? ExactProjectionWithdrawalDisposition.Completed
|
|
: ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
});
|
|
using (fixture)
|
|
{
|
|
LiveEntityRecord orphan = fixture.Spawn(0x70000269u, generation: 1);
|
|
|
|
TargetInvocationException error = Assert.Throws<TargetInvocationException>(() =>
|
|
fixture.InvokeOrphanRemoval(orphan));
|
|
Assert.IsType<InvalidOperationException>(error.InnerException);
|
|
|
|
fixture.Controller.Tick();
|
|
|
|
Assert.Equal(2, attempts);
|
|
Assert.False(orphan.IsSpatiallyProjected);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void RecoveryContinuation_PostCommitFailureIsNotReplayed()
|
|
{
|
|
ControllerFixture? fixture = null;
|
|
fixture = new ControllerFixture((record, positionVersion, projectionVersion) =>
|
|
{
|
|
bool completed = fixture!.Live.WithdrawLiveEntityProjection(
|
|
record,
|
|
positionVersion,
|
|
projectionVersion);
|
|
return new(
|
|
completed
|
|
? ExactProjectionWithdrawalDisposition.Completed
|
|
: ExactProjectionWithdrawalDisposition.Superseded,
|
|
Failure: null);
|
|
});
|
|
using (fixture)
|
|
{
|
|
LiveEntityRecord record = fixture.Spawn(0x70000270u, generation: 1);
|
|
int continuations = 0;
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
fixture.Controller.OnChildBecameUnparented(
|
|
0x70000270u,
|
|
() =>
|
|
{
|
|
continuations++;
|
|
Assert.True(fixture.Live.RebucketLiveEntity(
|
|
0x70000270u,
|
|
0x01010001u));
|
|
throw new InvalidOperationException("observer failed after recovery");
|
|
}));
|
|
|
|
fixture.Controller.Tick();
|
|
|
|
Assert.Equal(1, continuations);
|
|
Assert.True(record.IsSpatiallyProjected);
|
|
}
|
|
}
|
|
|
|
private static LiveEntityRecord ChildRecord() =>
|
|
LiveEntityTestFixture.CreateExactProjectionRecord(
|
|
new WorldSession.EntitySpawn(
|
|
0x70000100u,
|
|
new CreateObject.ServerPosition(
|
|
0x01010001u, 0f, 0f, 0f, 1f, 0f, 0f, 0f),
|
|
0x02000001u,
|
|
Array.Empty<CreateObject.AnimPartChange>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
BasePaletteId: null,
|
|
ObjScale: null,
|
|
Name: "attached fixture",
|
|
ItemType: null,
|
|
MotionState: null,
|
|
MotionTableId: null,
|
|
InstanceSequence: 1));
|
|
|
|
private sealed class ControllerFixture : IDisposable
|
|
{
|
|
private const uint Cell = 0x01010001u;
|
|
private readonly DeferredLiveEntityRuntimeComponentLifecycle _lifecycle = new();
|
|
private readonly Setup _setup;
|
|
|
|
internal ControllerFixture(
|
|
Func<LiveEntityRecord, ulong, ulong, ExactProjectionWithdrawalOutcome>
|
|
withdraw)
|
|
{
|
|
Spatial.AddLandblock(new LoadedLandblock(
|
|
(Cell & 0xFFFF0000u) | 0xFFFFu,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
Live = LiveEntityRuntimeFixture.Create(
|
|
Spatial,
|
|
new DelegateLiveEntityResourceLifecycle(_ => { }, _ => { }),
|
|
_lifecycle);
|
|
_setup = new Setup
|
|
{
|
|
HoldingLocations =
|
|
{
|
|
[(ParentLocation)0] = new LocationType
|
|
{
|
|
PartId = -1,
|
|
Frame = new Frame { Orientation = Quaternion.Identity },
|
|
},
|
|
},
|
|
};
|
|
IDatReaderWriter dat = DispatchProxy.Create<IDatReaderWriter, NullDatProxy>();
|
|
((NullDatProxy)(object)dat).Setup = _setup;
|
|
Controller = new EquippedChildRenderController(
|
|
dat,
|
|
new object(),
|
|
Objects,
|
|
Live,
|
|
Poses,
|
|
update => Live.TryApplyParent(update, out _),
|
|
withdraw);
|
|
_lifecycle.Bind(new DelegateLiveEntityRuntimeComponentLifecycle(
|
|
Controller.OnLogicalTeardown));
|
|
}
|
|
|
|
internal GpuWorldState Spatial { get; } = new();
|
|
internal ClientObjectTable Objects { get; } = new();
|
|
internal EntityEffectPoseRegistry Poses { get; } = new();
|
|
internal LiveEntityRuntime Live { get; }
|
|
internal EquippedChildRenderController Controller { get; }
|
|
|
|
internal LiveEntityRecord Spawn(
|
|
uint guid,
|
|
ushort generation,
|
|
LiveEntityProjectionKind kind = LiveEntityProjectionKind.World)
|
|
{
|
|
RuntimeEntityRecord canonical =
|
|
RegisterOnly(guid, generation, hasPosition: true);
|
|
return Materialize(canonical, kind);
|
|
}
|
|
|
|
internal RuntimeEntityRecord RegisterOnly(
|
|
uint guid,
|
|
ushort generation,
|
|
bool hasPosition,
|
|
bool hasSetup = true)
|
|
{
|
|
WorldSession.EntitySpawn spawn = SpawnData(guid, generation);
|
|
if (!hasPosition)
|
|
spawn = spawn with { Position = null };
|
|
if (!hasSetup)
|
|
spawn = spawn with { SetupTableId = null };
|
|
return Assert.IsType<RuntimeEntityRecord>(
|
|
Live.RegisterLiveEntity(spawn).Canonical);
|
|
}
|
|
|
|
internal LiveEntityRecord Materialize(
|
|
RuntimeEntityRecord canonical,
|
|
LiveEntityProjectionKind kind = LiveEntityProjectionKind.World)
|
|
{
|
|
WorldEntity? entity = Live.MaterializeLiveEntity(
|
|
canonical,
|
|
Cell,
|
|
id => new WorldEntity
|
|
{
|
|
Id = id,
|
|
ServerGuid = canonical.ServerGuid,
|
|
SourceGfxObjOrSetupId = 0x02000001u,
|
|
Position = Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
ParentCellId = Cell,
|
|
},
|
|
kind,
|
|
initializeProjection: null,
|
|
out LiveEntityRecord? projected);
|
|
Assert.NotNull(entity);
|
|
LiveEntityRecord record =
|
|
Assert.IsType<LiveEntityRecord>(projected);
|
|
record.HasPartArray = true;
|
|
return record;
|
|
}
|
|
|
|
internal static WorldSession.EntitySpawn SpawnData(uint guid, ushort generation) => new(
|
|
guid,
|
|
new CreateObject.ServerPosition(
|
|
Cell, 0f, 0f, 0f, 1f, 0f, 0f, 0f),
|
|
0x02000001u,
|
|
Array.Empty<CreateObject.AnimPartChange>(),
|
|
Array.Empty<CreateObject.TextureChange>(),
|
|
Array.Empty<CreateObject.SubPaletteSwap>(),
|
|
BasePaletteId: null,
|
|
ObjScale: null,
|
|
Name: "attached fixture",
|
|
ItemType: null,
|
|
MotionState: null,
|
|
MotionTableId: null,
|
|
InstanceSequence: generation);
|
|
|
|
internal void InstallAttached(
|
|
LiveEntityRecord parent,
|
|
LiveEntityRecord child)
|
|
{
|
|
Type attachedType = typeof(EquippedChildRenderController).GetNestedType(
|
|
"AttachedChild",
|
|
BindingFlags.NonPublic)!;
|
|
object attached = Activator.CreateInstance(
|
|
attachedType,
|
|
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
|
|
binder: null,
|
|
args:
|
|
[
|
|
parent,
|
|
child,
|
|
parent.ServerGuid,
|
|
child.ServerGuid,
|
|
(ParentLocation)0,
|
|
(Placement)0,
|
|
_setup,
|
|
_setup,
|
|
Array.Empty<MeshRef>(),
|
|
Array.Empty<bool>(),
|
|
Array.Empty<Matrix4x4>(),
|
|
Array.Empty<MeshRef>(),
|
|
1f,
|
|
child.WorldEntity!,
|
|
],
|
|
culture: null)!;
|
|
FieldInfo mapField = typeof(EquippedChildRenderController).GetField(
|
|
"_attachedByChild",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
var map = (IDictionary)mapField.GetValue(Controller)!;
|
|
map.Add(child.ProjectionKey!.Value, attached);
|
|
}
|
|
|
|
internal void CommitRenderedRelation(ParentAttachmentRelation relation)
|
|
{
|
|
Live.ParentAttachments.AcceptCreateObjectRelation(relation);
|
|
Assert.True(Live.ParentAttachments.CommitProjection(relation));
|
|
Live.ParentAttachments.MarkProjected(
|
|
relation,
|
|
ParentProjectionCandidateKind.Recovery);
|
|
}
|
|
|
|
internal void InvokeOrdinaryRemoval(uint guid)
|
|
{
|
|
MethodInfo method = typeof(EquippedChildRenderController).GetMethod(
|
|
"TearDownCurrentObjectProjections",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
method.Invoke(Controller, [guid]);
|
|
}
|
|
|
|
internal void InvokeDetachedRemoval(uint guid)
|
|
{
|
|
MethodInfo method = typeof(EquippedChildRenderController).GetMethod(
|
|
"BeginDetachedRemoval",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
method.Invoke(Controller, [guid]);
|
|
}
|
|
|
|
internal void InvokeOrphanRemoval(LiveEntityRecord record)
|
|
{
|
|
FieldInfo mapField = typeof(EquippedChildRenderController).GetField(
|
|
"_pendingOrphanRemovalByChild",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
object map = mapField.GetValue(Controller)!;
|
|
MethodInfo method = typeof(EquippedChildRenderController).GetMethod(
|
|
"BeginProjectionSubtreeWithdrawal",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
method.Invoke(Controller, [map, record, false, false]);
|
|
}
|
|
|
|
public void Dispose() => Controller.Dispose();
|
|
}
|
|
|
|
private class NullDatProxy : DispatchProxy
|
|
{
|
|
internal Setup? Setup { get; set; }
|
|
|
|
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
|
|
{
|
|
if (targetMethod?.Name == "Get"
|
|
&& targetMethod.ReturnType == typeof(Setup))
|
|
{
|
|
return Setup;
|
|
}
|
|
if (targetMethod?.ReturnType == typeof(void))
|
|
return null;
|
|
if (targetMethod?.ReturnType.IsValueType == true)
|
|
return Activator.CreateInstance(targetMethod.ReturnType);
|
|
return null;
|
|
}
|
|
}
|
|
}
|