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.
1279 lines
49 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|