acdream/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs
Erik f7442d13e9 refactor(runtime): move accepted entity wire state
Move the presentation-free inbound physics timestamp/snapshot authority and parent-relation state into AcDream.Runtime.Entities without changing their control flow. Move their dedicated tests with them and keep App consumers as borrowers during the staged J3 cutover.

Validated by 26 focused Runtime tests, 232 focused App tests, the Release solution build, and 8,429 complete Release tests with five existing skips.

Co-authored-by: Codex <codex@openai.com>
2026-07-25 19:52:29 +02:00

1341 lines
51 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.RegisterLiveEntity(
ControllerFixture.SpawnData(0x70000201u, generation: 2)).Record!;
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(uint _) =>
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();
long before = GC.GetAllocatedBytesForCurrentThread();
for (int i = 0; i < 1_000; i++)
fixture.Controller.ReconcileSpatialMutations();
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
Assert.Equal(0L, allocated);
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));
LiveEntityRecord child = fixture.RegisterOnly(
childGuid,
generation: 1,
hasPosition: true);
fixture.Materialize(child);
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);
LiveEntityRecord 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.Null(child.WorldEntity);
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.NotNull(child.WorldEntity);
Assert.True(child.IsSpatiallyProjected);
Assert.Equal(LiveEntityProjectionKind.Attached, child.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,
};
LiveEntityRecord child = fixture.Live.RegisterLiveEntity(childSpawn).Record!;
fixture.Controller.OnSpawn(childSpawn);
var expected = new ParentAttachmentRelation(
parent.ServerGuid,
child.ServerGuid,
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.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,
};
LiveEntityRecord child = fixture.Live.RegisterLiveEntity(childSpawn).Record!;
fixture.Controller.OnSpawn(childSpawn);
fixture.Poses.Publish(parent.WorldEntity!, Array.Empty<Matrix4x4>());
fixture.Controller.OnPosePublished(parent.ServerGuid);
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,
};
LiveEntityRecord grandchild = fixture.Live.RegisterLiveEntity(
grandchildSpawn).Record!;
fixture.Controller.OnSpawn(grandchildSpawn);
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);
LiveEntityRecord child = fixture.RegisterOnly(
0x7000025Du,
generation: 1,
hasPosition: true,
hasSetup: false);
fixture.Materialize(child);
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);
LiveEntityRecord 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 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() => new(
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 = new LiveEntityRuntime(
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)
{
LiveEntityRecord record = RegisterOnly(guid, generation, hasPosition: true);
Materialize(record, kind);
return record;
}
internal LiveEntityRecord 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 Live.RegisterLiveEntity(spawn).Record!;
}
internal void Materialize(
LiveEntityRecord record,
LiveEntityProjectionKind kind = LiveEntityProjectionKind.World)
{
Live.MaterializeLiveEntity(
record.ServerGuid,
Cell,
id => new WorldEntity
{
Id = id,
ServerGuid = record.ServerGuid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty<MeshRef>(),
ParentCellId = Cell,
},
kind);
record.HasPartArray = true;
}
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.ServerGuid, 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;
}
}
}