acdream/tests/AcDream.App.Tests/Rendering/EquippedChildProjectionWithdrawalTests.cs
Erik fe5514967c refactor(world): extract live appearance and parenting
Move ObjDesc, Parent, Pickup, child-ready, and retained projection recovery into LiveEntityHydrationController while preserving retail timestamp and leave-world semantics. Pin ready publication to exact projection authority and restore attached descendant chains synchronously.
2026-07-21 18:10:07 +02:00

1279 lines
49 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 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 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;
}
}
}