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.
This commit is contained in:
parent
40352d5a7a
commit
fe5514967c
9 changed files with 1098 additions and 199 deletions
|
|
@ -22,6 +22,10 @@ public sealed class GameWindowLiveEntityCompositionTests
|
|||
[InlineData("RehydrateServerEntitiesForLandblock")]
|
||||
[InlineData("TryInitializeLiveCenter")]
|
||||
[InlineData("CompleteLiveEntityReady")]
|
||||
[InlineData("OnLiveAppearanceUpdated")]
|
||||
[InlineData("OnLiveEntityPickedUp")]
|
||||
[InlineData("OnLiveParentUpdated")]
|
||||
[InlineData("TryAcceptParentForRender")]
|
||||
public void GameWindow_DoesNotReacquireExtractedLiveEntityBodies(string methodName)
|
||||
{
|
||||
Assert.Null(typeof(GameWindow).GetMethod(methodName, PrivateImplementation));
|
||||
|
|
|
|||
|
|
@ -12,6 +12,25 @@ namespace AcDream.App.Tests.World;
|
|||
|
||||
public sealed class LiveEntityHydrationControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParentAcceptanceBridge_IsFailFastAndSingleAssignment()
|
||||
{
|
||||
var bridge = new DeferredLiveEntityParentAcceptance();
|
||||
var update = new ParentEvent.Parsed(1, 2, 3, 4, 5, 6);
|
||||
Assert.Throws<InvalidOperationException>(() => bridge.TryAccept(update));
|
||||
|
||||
ParentEvent.Parsed accepted = default;
|
||||
bridge.Bind(value =>
|
||||
{
|
||||
accepted = value;
|
||||
return true;
|
||||
});
|
||||
|
||||
Assert.True(bridge.TryAccept(update));
|
||||
Assert.Equal(update, accepted);
|
||||
Assert.Throws<InvalidOperationException>(() => bridge.Bind(_ => false));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FreshCreate_PublishesCanonicalOrderAndReadyAfterMaterialization()
|
||||
{
|
||||
|
|
@ -93,10 +112,7 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
LiveEntityRecord record = fixture.Record;
|
||||
Assert.Null(record.WorldEntity);
|
||||
|
||||
Assert.True(fixture.Controller.ApplyAppearance(
|
||||
record,
|
||||
record.Snapshot,
|
||||
appearanceUpdate: null));
|
||||
Assert.True(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u)));
|
||||
|
||||
Assert.NotNull(record.WorldEntity);
|
||||
Assert.Equal(2, fixture.Materializer.Calls.Count);
|
||||
|
|
@ -105,6 +121,324 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
Assert.Equal(1, fixture.Ready.PublishCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppearanceUpdate_ReplacesOnlyProjectionAndPreservesExactOwners()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
LiveEntityRecord record = fixture.Record;
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
int applied = 0;
|
||||
fixture.Controller.AppearanceApplied += _ => applied++;
|
||||
|
||||
Assert.True(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u)));
|
||||
|
||||
Assert.Same(record, fixture.Record);
|
||||
Assert.Same(entity, record.WorldEntity);
|
||||
Assert.Equal((uint)0x04000022u, record.Snapshot.BasePaletteId);
|
||||
Assert.False(record.AppearanceProjectionSynchronizationPending);
|
||||
Assert.Equal(LiveProjectionPurpose.AppearanceMutation, fixture.Materializer.Calls[^1].Purpose);
|
||||
Assert.Equal(1, fixture.Resources.RegisterCount);
|
||||
Assert.Equal(0, fixture.Resources.UnregisterCount);
|
||||
Assert.Equal(1, applied);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestedAppearanceUpdate_ReplaysNewestCanonicalSnapshotAfterOuterPublication()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
int applied = 0;
|
||||
fixture.Controller.AppearanceApplied += _ => applied++;
|
||||
bool nested = false;
|
||||
bool nestedResult = true;
|
||||
fixture.Materializer.BeforeMaterialize = (_, _) =>
|
||||
{
|
||||
if (fixture.Materializer.Calls[^1].Purpose is not LiveProjectionPurpose.AppearanceMutation
|
||||
|| nested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
nested = true;
|
||||
nestedResult = fixture.Controller.OnAppearance(ObjDesc(3, 0x04000033u));
|
||||
};
|
||||
|
||||
Assert.True(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u)));
|
||||
|
||||
Assert.False(nestedResult);
|
||||
Assert.Equal((uint)0x04000033u, fixture.Record.Snapshot.BasePaletteId);
|
||||
Assert.False(fixture.Record.AppearanceProjectionSynchronizationPending);
|
||||
Assert.Equal(
|
||||
2,
|
||||
fixture.Materializer.Calls.Count(call =>
|
||||
call.Purpose is LiveProjectionPurpose.AppearanceMutation));
|
||||
Assert.Equal(1, applied);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppearancePublicationFailure_RetainsObligationForLandblockRetry()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
int applied = 0;
|
||||
fixture.Controller.AppearanceApplied += _ => applied++;
|
||||
fixture.Materializer.ThrowAfterMaterializePurposeOnce =
|
||||
LiveProjectionPurpose.AppearanceMutation;
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u)));
|
||||
Assert.True(fixture.Record.AppearanceProjectionSynchronizationPending);
|
||||
Assert.Equal(0, applied);
|
||||
|
||||
fixture.Controller.OnLandblockLoaded(Cell);
|
||||
|
||||
Assert.False(fixture.Record.AppearanceProjectionSynchronizationPending);
|
||||
Assert.Equal((uint)0x04000022u, fixture.Record.Snapshot.BasePaletteId);
|
||||
Assert.Equal(1, fixture.Resources.RegisterCount);
|
||||
Assert.Equal(1, applied);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttachedAppearance_UsesRelationshipProjectionWithoutWorldPosition()
|
||||
{
|
||||
const uint parentGuid = 0x70000002u;
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
LiveEntityRecord record = fixture.Record;
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
Assert.True(fixture.Runtime.TryApplyCreateParent(
|
||||
new CreateParentUpdate(Guid, parentGuid, 1, 0, 1, 2),
|
||||
out _));
|
||||
Assert.True(fixture.Runtime.CommitStagedParent(
|
||||
new ParentAttachmentRelation(
|
||||
parentGuid,
|
||||
Guid,
|
||||
ParentLocation: 1,
|
||||
PlacementId: 0,
|
||||
ParentInstanceSequence: 0,
|
||||
ChildPositionSequence: 2),
|
||||
out _));
|
||||
record.ProjectionKind = LiveEntityProjectionKind.Attached;
|
||||
record.FullCellId = 0u;
|
||||
int materializerCalls = fixture.Materializer.Calls.Count;
|
||||
int relationshipCalls = 0;
|
||||
int applied = 0;
|
||||
fixture.Relationships.ApplyAttachedAppearance = (expected, version) =>
|
||||
{
|
||||
relationshipCalls++;
|
||||
Assert.Same(record, expected);
|
||||
Assert.True(fixture.Runtime.IsCurrentObjDescAuthority(expected, version));
|
||||
Assert.Null(expected.Snapshot.Position);
|
||||
Assert.Equal((uint)0x04000022u, expected.Snapshot.BasePaletteId);
|
||||
return true;
|
||||
};
|
||||
fixture.Controller.AppearanceApplied += _ => applied++;
|
||||
|
||||
Assert.True(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u)));
|
||||
|
||||
Assert.Same(entity, record.WorldEntity);
|
||||
Assert.Equal(LiveEntityProjectionKind.Attached, record.ProjectionKind);
|
||||
Assert.False(record.AppearanceProjectionSynchronizationPending);
|
||||
Assert.Equal(materializerCalls, fixture.Materializer.Calls.Count);
|
||||
Assert.Equal(1, relationshipCalls);
|
||||
Assert.Equal(1, applied);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1)]
|
||||
[InlineData(0)]
|
||||
public void StaleOrEqualAppearance_HasNoProjectionOrNotificationSideEffects(
|
||||
ushort sequence)
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
int calls = fixture.Materializer.Calls.Count;
|
||||
int applied = 0;
|
||||
fixture.Controller.AppearanceApplied += _ => applied++;
|
||||
|
||||
Assert.False(fixture.Controller.OnAppearance(
|
||||
ObjDesc(sequence, 0x04000022u)));
|
||||
|
||||
Assert.Equal(calls, fixture.Materializer.Calls.Count);
|
||||
Assert.Equal(0, applied);
|
||||
Assert.False(fixture.Record.AppearanceProjectionSynchronizationPending);
|
||||
Assert.Null(fixture.Record.Snapshot.BasePaletteId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AppearanceSequenceWrap_PublishesExactlyOnce()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
WorldSession.EntitySpawn spawn = Spawn(Generation: 1, PositionSequence: 1);
|
||||
spawn = spawn with
|
||||
{
|
||||
Physics = spawn.Physics!.Value with
|
||||
{
|
||||
Timestamps = spawn.Physics.Value.Timestamps with
|
||||
{
|
||||
ObjDesc = 0xFFFF,
|
||||
},
|
||||
},
|
||||
};
|
||||
fixture.Controller.OnCreate(spawn);
|
||||
int applied = 0;
|
||||
fixture.Controller.AppearanceApplied += _ => applied++;
|
||||
|
||||
Assert.True(fixture.Controller.OnAppearance(ObjDesc(0, 0x04000022u)));
|
||||
|
||||
Assert.False(fixture.Record.AppearanceProjectionSynchronizationPending);
|
||||
Assert.Equal((uint)0x04000022u, fixture.Record.Snapshot.BasePaletteId);
|
||||
Assert.Equal(1, applied);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FailedAppearance_IsClearedAtGenerationReplacementAndCannotLeak()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
LiveEntityRecord retired = fixture.Record;
|
||||
fixture.Materializer.SkipMaterializationCount = 1;
|
||||
Assert.False(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u)));
|
||||
Assert.True(retired.AppearanceProjectionSynchronizationPending);
|
||||
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 2, PositionSequence: 1));
|
||||
LiveEntityRecord replacement = fixture.Record;
|
||||
fixture.Controller.OnLandblockLoaded(Cell);
|
||||
|
||||
Assert.NotSame(retired, replacement);
|
||||
Assert.False(retired.AppearanceProjectionSynchronizationPending);
|
||||
Assert.False(retired.AppearanceHydrationInProgress);
|
||||
Assert.False(replacement.AppearanceProjectionSynchronizationPending);
|
||||
Assert.Null(replacement.Snapshot.BasePaletteId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pickup_LeavesWorldWithoutEndingLogicalOrResourceIdentity()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
LiveEntityRecord record = fixture.Record;
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
fixture.Relationships.OnUnparentAction = _ =>
|
||||
fixture.Runtime.WithdrawLiveEntityProjection(record)
|
||||
? ChildUnparentDisposition.Completed
|
||||
: ChildUnparentDisposition.Superseded;
|
||||
|
||||
Assert.True(fixture.Controller.OnPickup(
|
||||
new PickupEvent.Parsed(Guid, 1, 2)));
|
||||
|
||||
Assert.Same(record, fixture.Record);
|
||||
Assert.Same(entity, record.WorldEntity);
|
||||
Assert.Equal(0u, record.FullCellId);
|
||||
Assert.False(record.IsSpatiallyProjected);
|
||||
Assert.True(record.ResourcesRegistered);
|
||||
Assert.Equal(1, fixture.Resources.RegisterCount);
|
||||
Assert.Equal(0, fixture.Resources.UnregisterCount);
|
||||
Assert.Contains($"unparent:{Guid:X8}", fixture.Operations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PositionAfterPickup_ReentersWithSameEntityBodyAndResources()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
LiveEntityRecord record = fixture.Record;
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
var body = new PhysicsBody();
|
||||
record.PhysicsBody = body;
|
||||
fixture.Relationships.OnUnparentAction = _ =>
|
||||
fixture.Runtime.WithdrawLiveEntityProjection(record)
|
||||
? ChildUnparentDisposition.Completed
|
||||
: ChildUnparentDisposition.Superseded;
|
||||
Assert.True(fixture.Controller.OnPickup(
|
||||
new PickupEvent.Parsed(Guid, 1, 2)));
|
||||
int applied = 0;
|
||||
fixture.Controller.AppearanceApplied += _ => applied++;
|
||||
Assert.False(fixture.Controller.OnAppearance(ObjDesc(2, 0x04000022u)));
|
||||
Assert.True(record.AppearanceProjectionSynchronizationPending);
|
||||
Assert.Equal(0, applied);
|
||||
|
||||
Assert.True(fixture.Runtime.TryApplyPosition(
|
||||
new WorldSession.EntityPositionUpdate(
|
||||
Guid,
|
||||
new CreateObject.ServerPosition(
|
||||
Cell, 14f, 12f, 6f, 1f, 0f, 0f, 0f),
|
||||
Velocity: null,
|
||||
PlacementId: 0,
|
||||
IsGrounded: true,
|
||||
InstanceSequence: 1,
|
||||
PositionSequence: 3,
|
||||
TeleportSequence: 0,
|
||||
ForcePositionSequence: 0),
|
||||
isLocalPlayer: false,
|
||||
forcePositionRotation: null,
|
||||
currentLocalVelocity: null,
|
||||
out _,
|
||||
out WorldSession.EntitySpawn accepted,
|
||||
out _));
|
||||
ulong positionAuthority = record.PositionAuthorityVersion;
|
||||
|
||||
Assert.True(fixture.Controller.RecoverProjection(
|
||||
record,
|
||||
positionAuthority,
|
||||
accepted));
|
||||
|
||||
Assert.Same(entity, record.WorldEntity);
|
||||
Assert.Same(body, record.PhysicsBody);
|
||||
Assert.True(record.IsSpatiallyProjected);
|
||||
Assert.False(record.AppearanceProjectionSynchronizationPending);
|
||||
Assert.Equal((uint)0x04000022u, record.Snapshot.BasePaletteId);
|
||||
Assert.Equal(1, applied);
|
||||
Assert.Equal(1, fixture.Resources.RegisterCount);
|
||||
Assert.Equal(0, fixture.Resources.UnregisterCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParentEvent_IsRetainedByRelationshipOwnerAndAcceptedThroughHydrationGate()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
const uint parentGuid = 0x70000002u;
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
fixture.Runtime.RegisterLiveEntity(
|
||||
Spawn(Generation: 4, PositionSequence: 1) with { Guid = parentGuid });
|
||||
bool accepted = false;
|
||||
fixture.Relationships.OnParentAction = update =>
|
||||
accepted = fixture.Controller.TryAcceptParentForProjection(update);
|
||||
var update = new ParentEvent.Parsed(
|
||||
parentGuid,
|
||||
Guid,
|
||||
ParentLocation: 1,
|
||||
PlacementId: 0,
|
||||
ParentInstanceSequence: 4,
|
||||
ChildPositionSequence: 2);
|
||||
|
||||
fixture.Controller.OnParent(update);
|
||||
|
||||
Assert.True(accepted);
|
||||
Assert.Equal((ushort)2, fixture.Record.Snapshot.PositionSequence);
|
||||
Assert.Contains($"parent:{Guid:X8}", fixture.Operations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SameGenerationCreateParent_AdvancesSharedPositionAndPublishesRelation()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
var update = new CreateParentUpdate(
|
||||
Guid,
|
||||
ParentGuid: 0x70000002u,
|
||||
ParentLocation: 3,
|
||||
PlacementId: 0,
|
||||
ChildInstanceSequence: 1,
|
||||
ChildPositionSequence: 2);
|
||||
|
||||
Assert.True(fixture.Controller.OnCreateParentAccepted(update));
|
||||
|
||||
Assert.Equal((ushort)2, fixture.Record.Snapshot.PositionSequence);
|
||||
Assert.Contains($"create-parent:{Guid:X8}", fixture.Operations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SameOlderNewerAndWrappedGenerations_RouteOrReplaceExactlyOnce()
|
||||
{
|
||||
|
|
@ -805,9 +1139,7 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
LiveEntityProjectionKind.Attached);
|
||||
Assert.Same(retained, attached);
|
||||
fixture.Controller.OnEntityReady(
|
||||
new LiveEntityReadyCandidate(
|
||||
record,
|
||||
record.CreateIntegrationVersion));
|
||||
LiveEntityReadyCandidate.Capture(record));
|
||||
};
|
||||
bool refreshed = false;
|
||||
fixture.Materializer.BeforeMaterialize = (_, spawn) =>
|
||||
|
|
@ -880,9 +1212,8 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
},
|
||||
LiveEntityProjectionKind.Attached);
|
||||
Assert.NotNull(attached);
|
||||
fixture.Controller.OnEntityReady(new LiveEntityReadyCandidate(
|
||||
record,
|
||||
record.CreateIntegrationVersion));
|
||||
fixture.Controller.OnEntityReady(
|
||||
LiveEntityReadyCandidate.Capture(record));
|
||||
};
|
||||
|
||||
fixture.Controller.OnCreate(CelllessSpawn(
|
||||
|
|
@ -945,7 +1276,8 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
_ => { present++; return Stage(1); },
|
||||
_ => { replay++; return Stage(2); });
|
||||
|
||||
Assert.False(publisher.Publish(expected));
|
||||
Assert.False(publisher.Publish(
|
||||
LiveEntityReadyCandidate.Capture(expected)));
|
||||
Assert.Equal(expectedPrepare, prepare);
|
||||
Assert.Equal(expectedPresent, present);
|
||||
Assert.Equal(expectedReplay, replay);
|
||||
|
|
@ -953,6 +1285,52 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
Assert.Equal(2UL, fixture.Record.CreateIntegrationVersion);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 1, 0, 0)]
|
||||
[InlineData(1, 1, 1, 0)]
|
||||
[InlineData(2, 1, 1, 1)]
|
||||
public void ReadyPublisher_RejectsSameIdentityProjectionChangeBetweenStages(
|
||||
int withdrawalStage,
|
||||
int expectedPrepare,
|
||||
int expectedPresent,
|
||||
int expectedReplay)
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
LiveEntityRecord expected = fixture.Record;
|
||||
LiveEntityReadyCandidate candidate =
|
||||
LiveEntityReadyCandidate.Capture(expected);
|
||||
int prepare = 0;
|
||||
int present = 0;
|
||||
int replay = 0;
|
||||
bool withdrawn = false;
|
||||
bool Stage(int stage)
|
||||
{
|
||||
if (!withdrawn && withdrawalStage == stage)
|
||||
{
|
||||
withdrawn = true;
|
||||
Assert.True(fixture.Runtime.TryApplyPickup(
|
||||
new PickupEvent.Parsed(Guid, 1, 2),
|
||||
out _));
|
||||
Assert.True(fixture.Runtime.WithdrawLiveEntityProjection(expected));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
var publisher = new LiveEntityReadyPublisher(
|
||||
fixture.Runtime,
|
||||
_ => { prepare++; return Stage(0); },
|
||||
_ => { present++; return Stage(1); },
|
||||
_ => { replay++; return Stage(2); });
|
||||
|
||||
Assert.False(publisher.Publish(candidate));
|
||||
Assert.Equal(expectedPrepare, prepare);
|
||||
Assert.Equal(expectedPresent, present);
|
||||
Assert.Equal(expectedReplay, replay);
|
||||
Assert.Same(expected, fixture.Record);
|
||||
Assert.False(expected.IsSpatiallyProjected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EquippedChildReadyCandidate_RejectsVersionAdvancedByPoseCallback()
|
||||
{
|
||||
|
|
@ -970,9 +1348,35 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
|
||||
Assert.False(EquippedChildRenderController.PublishEntityReadyExact(
|
||||
fixture.Runtime,
|
||||
record,
|
||||
capturedCreateIntegrationVersion,
|
||||
entity,
|
||||
LiveEntityReadyCandidate.Capture(record) with
|
||||
{
|
||||
CreateIntegrationVersion = capturedCreateIntegrationVersion,
|
||||
WorldEntity = entity,
|
||||
},
|
||||
_ => published = true));
|
||||
Assert.False(published);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EquippedChildReadyCandidate_RejectsPickupWithdrawalByPoseCallback()
|
||||
{
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
LiveEntityRecord record = fixture.Record;
|
||||
LiveEntityReadyCandidate candidate =
|
||||
LiveEntityReadyCandidate.Capture(record);
|
||||
|
||||
// Models ProjectionPoseReady accepting Pickup and withdrawing the
|
||||
// retained projection before EntityReady is emitted.
|
||||
Assert.True(fixture.Runtime.TryApplyPickup(
|
||||
new PickupEvent.Parsed(Guid, 1, 2),
|
||||
out _));
|
||||
Assert.True(fixture.Runtime.WithdrawLiveEntityProjection(record));
|
||||
bool published = false;
|
||||
|
||||
Assert.False(EquippedChildRenderController.PublishEntityReadyExact(
|
||||
fixture.Runtime,
|
||||
candidate,
|
||||
_ => published = true));
|
||||
Assert.False(published);
|
||||
}
|
||||
|
|
@ -1256,14 +1660,46 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
}
|
||||
|
||||
private sealed class RecordingRelationships(List<string> operations)
|
||||
: ILiveEntitySpawnRelationshipSink
|
||||
: ILiveEntityRelationshipProjection
|
||||
{
|
||||
public Action<WorldSession.EntitySpawn>? OnSpawnAction { get; set; }
|
||||
public Action<ParentEvent.Parsed>? OnParentAction { get; set; }
|
||||
public Action<CreateParentUpdate>? OnCreateParentAction { get; set; }
|
||||
public Func<uint, ChildUnparentDisposition>? OnUnparentAction { get; set; }
|
||||
public Func<LiveEntityRecord, ulong, bool>? ApplyAttachedAppearance { get; set; }
|
||||
public void OnSpawn(WorldSession.EntitySpawn spawn)
|
||||
{
|
||||
operations.Add($"relationship:{spawn.InstanceSequence}");
|
||||
OnSpawnAction?.Invoke(spawn);
|
||||
}
|
||||
|
||||
public void OnParent(ParentEvent.Parsed update)
|
||||
{
|
||||
operations.Add($"parent:{update.ChildGuid:X8}");
|
||||
OnParentAction?.Invoke(update);
|
||||
}
|
||||
|
||||
public void OnCreateParentAccepted(CreateParentUpdate update)
|
||||
{
|
||||
operations.Add($"create-parent:{update.ChildGuid:X8}");
|
||||
OnCreateParentAction?.Invoke(update);
|
||||
}
|
||||
|
||||
public ChildUnparentDisposition OnChildBecameUnparented(uint childGuid)
|
||||
{
|
||||
operations.Add($"unparent:{childGuid:X8}");
|
||||
return OnUnparentAction?.Invoke(childGuid)
|
||||
?? ChildUnparentDisposition.Completed;
|
||||
}
|
||||
|
||||
public bool TryApplyAttachedAppearance(
|
||||
LiveEntityRecord record,
|
||||
ulong objDescAuthorityVersion)
|
||||
{
|
||||
operations.Add($"attached-appearance:{record.ServerGuid:X8}");
|
||||
return ApplyAttachedAppearance?.Invoke(record, objDescAuthorityVersion)
|
||||
?? false;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class RecordingReadyPublisher(List<string> operations)
|
||||
|
|
@ -1272,8 +1708,9 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
public int PublishCount { get; private set; }
|
||||
public int FailPublishCount { get; set; }
|
||||
public Action<LiveEntityRecord>? BeforePublish { get; set; }
|
||||
public bool Publish(LiveEntityRecord expectedRecord)
|
||||
public bool Publish(LiveEntityReadyCandidate candidate)
|
||||
{
|
||||
LiveEntityRecord expectedRecord = candidate.Record;
|
||||
PublishCount++;
|
||||
operations.Add("ready");
|
||||
BeforePublish?.Invoke(expectedRecord);
|
||||
|
|
@ -1357,6 +1794,19 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
Physics: physics);
|
||||
}
|
||||
|
||||
private static ObjDescEvent.Parsed ObjDesc(
|
||||
ushort sequence,
|
||||
uint basePaletteId) =>
|
||||
new(
|
||||
Guid,
|
||||
new CreateObject.ModelData(
|
||||
basePaletteId,
|
||||
Array.Empty<CreateObject.SubPaletteSwap>(),
|
||||
Array.Empty<CreateObject.TextureChange>(),
|
||||
Array.Empty<CreateObject.AnimPartChange>()),
|
||||
InstanceSequence: 1,
|
||||
ObjDescSequence: sequence);
|
||||
|
||||
private static WorldSession.EntitySpawn CelllessSpawn(
|
||||
ushort PositionSequence,
|
||||
uint? parentGuid)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue