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

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

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

367 lines
15 KiB
C#

using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.Items;
using AcDream.Runtime.Entities;
namespace AcDream.Runtime.Tests.Entities;
public sealed class ParentAttachmentStateTests
{
[Fact]
public void ParentEventBeforeChildCreateResolvesToCanonicalAttachment()
{
const uint parentGuid = 0x70000100u;
const uint childGuid = 0x70000101u;
var inbound = new InboundPhysicsStateController();
var relations = new ParentAttachmentState();
inbound.AcceptCreate(Spawn(parentGuid, instance: 9, positionSequence: 1));
relations.Enqueue(new ParentEvent.Parsed(parentGuid, childGuid, 1, 2, 9, 5));
Resolve(relations, inbound, childGuid);
Assert.False(relations.TryGetProjection(childGuid, out _));
inbound.AcceptCreate(Spawn(childGuid, instance: 3, positionSequence: 4));
Resolve(relations, inbound, childGuid);
Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection));
Assert.Equal(parentGuid, projection.ParentGuid);
Assert.True(inbound.TryGetSnapshot(childGuid, out WorldSession.EntitySpawn child));
Assert.NotNull(child.Position);
Assert.Null(child.ParentGuid);
Assert.Equal((ushort)5, child.PositionSequence);
Assert.True(Commit(relations, inbound, projection, out child));
Assert.Null(child.Position);
Assert.Equal(parentGuid, child.ParentGuid);
}
[Fact]
public void MultipleQueuedRelationsRemainOrderedAndNewestAcceptedWins()
{
const uint parentGuid = 0x70000110u;
const uint childGuid = 0x70000111u;
var gate = new PhysicsTimestampGate();
gate.SeedForCreateObject(4, 0, 0, 0, 0, 0, 0, 0, 3);
var relations = new ParentAttachmentState();
relations.Enqueue(new ParentEvent.Parsed(parentGuid, childGuid, 1, 2, 9, 5));
relations.Enqueue(new ParentEvent.Parsed(parentGuid, childGuid, 1, 3, 9, 6));
relations.Resolve(
childGuid,
_ => true,
guid => guid == parentGuid ? (ushort)9 : null,
update => gate.TryAcceptPositionChannelEvent(3, update.ChildPositionSequence));
Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection));
Assert.Equal((ushort)5, projection.ChildPositionSequence);
Assert.Equal((uint)2, projection.PlacementId);
Assert.True(relations.CommitProjection(projection));
relations.MarkProjected(
projection,
ParentProjectionCandidateKind.Recovery);
relations.Resolve(
childGuid,
_ => true,
guid => guid == parentGuid ? (ushort)9 : null,
update => gate.TryAcceptPositionChannelEvent(3, update.ChildPositionSequence));
Assert.True(relations.TryGetStagedProjection(childGuid, out projection));
Assert.Equal((ushort)6, projection.ChildPositionSequence);
Assert.Equal((uint)3, projection.PlacementId);
Assert.True(relations.CommitProjection(projection));
relations.MarkProjected(
projection,
ParentProjectionCandidateKind.Recovery);
Assert.True(relations.RestoreLastAccepted(childGuid));
Assert.True(relations.TryGetProjection(childGuid, out projection));
Assert.Equal((ushort)6, projection.ChildPositionSequence);
}
[Fact]
public void RejectedStaleRelationCannotReplaceAcceptedCreateRelation()
{
const uint childGuid = 0x70000121u;
var gate = new PhysicsTimestampGate();
gate.SeedForCreateObject(10, 0, 0, 0, 0, 0, 0, 0, 3);
var relations = new ParentAttachmentState();
relations.AcceptCreateObjectRelation(new ParentAttachmentRelation(
0x70000120u, childGuid, 1, 2, 0, 10));
relations.Enqueue(new ParentEvent.Parsed(
0x70000122u, childGuid, 1, 4, 8, 9));
relations.Resolve(
childGuid,
_ => true,
guid => guid == 0x70000122u ? (ushort)8 : null,
update => gate.TryAcceptPositionChannelEvent(3, update.ChildPositionSequence));
Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection));
Assert.Equal(0x70000120u, projection.ParentGuid);
Assert.Equal((ushort)10, projection.ChildPositionSequence);
}
[Fact]
public void FutureParentGenerationDoesNotBlockCurrentRelationBehindIt()
{
const uint childGuid = 0x70000141u;
const uint parentGuid = 0x70000140u;
var gate = new PhysicsTimestampGate();
gate.SeedForCreateObject(4, 0, 0, 0, 0, 0, 0, 0, 3);
var relations = new ParentAttachmentState();
relations.Enqueue(new ParentEvent.Parsed(
parentGuid, childGuid, 1, 2, 10, 6));
relations.Enqueue(new ParentEvent.Parsed(
parentGuid, childGuid, 1, 3, 9, 5));
relations.Resolve(
childGuid,
_ => true,
guid => guid == parentGuid ? (ushort)9 : null,
update => gate.TryAcceptPositionChannelEvent(3, update.ChildPositionSequence));
Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection));
Assert.Equal((ushort)5, projection.ChildPositionSequence);
Assert.Equal((uint)3, projection.PlacementId);
Assert.Contains(childGuid, relations.ChildrenWaitingForParent(parentGuid));
}
[Fact]
public void FutureRelationSurvivesAtomicParentGenerationReplacement()
{
const uint parentGuid = 0x70000150u;
const uint childGuid = 0x70000151u;
var inbound = new InboundPhysicsStateController();
var relations = new ParentAttachmentState();
var objects = new ClientObjectTable();
inbound.AcceptCreate(Spawn(parentGuid, instance: 9, positionSequence: 1));
inbound.AcceptCreate(Spawn(childGuid, instance: 3, positionSequence: 4));
relations.AcceptCreateObjectRelation(new ParentAttachmentRelation(
parentGuid, childGuid, 1, 2, 0, 4));
relations.Enqueue(new ParentEvent.Parsed(
parentGuid, childGuid, 1, 3, 10, 5));
Resolve(relations, inbound, childGuid);
objects.Ingest(MinimalWeenie(parentGuid, "generation 9"));
objects.ObjectRemovalClassified += removal =>
{
if (removal.Reason == ClientObjectRemovalReason.GenerationReplacement)
relations.EndGeneration(removal.Object.ObjectId, removal.Generation);
};
inbound.AcceptCreate(Spawn(parentGuid, instance: 10, positionSequence: 1));
objects.ReplaceGeneration(MinimalWeenie(parentGuid, "generation 10"), 10);
Resolve(relations, inbound, childGuid);
Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection));
Assert.Equal(parentGuid, projection.ParentGuid);
Assert.Equal((ushort)10, projection.ParentInstanceSequence);
Assert.Equal((ushort)5, projection.ChildPositionSequence);
Assert.True(Commit(relations, inbound, projection, out _));
Assert.True(inbound.TryGetSnapshot(childGuid, out WorldSession.EntitySpawn child));
Assert.Equal(parentGuid, child.ParentGuid);
}
[Fact]
public void FutureParentRelationSurvivesChildGenerationReplacement()
{
const uint parentGuid = 0x70000180u;
const uint childGuid = 0x70000181u;
var relations = new ParentAttachmentState();
relations.Enqueue(new ParentEvent.Parsed(
parentGuid, childGuid, 1, 3, 10, 5));
// Parent generation 9 exists, so retail queues this blob against the
// future parent rather than against the already-live child.
relations.Resolve(
childGuid,
_ => true,
guid => guid == parentGuid ? (ushort)9 : null,
_ => throw new InvalidOperationException("future parent must not apply"));
relations.EndGeneration(childGuid, replacementGeneration: 4);
var replacementGate = new PhysicsTimestampGate();
replacementGate.SeedForCreateObject(4, 0, 0, 0, 0, 0, 0, 0, 4);
relations.Resolve(
childGuid,
_ => true,
guid => guid == parentGuid ? (ushort)10 : null,
update => replacementGate.TryAcceptPositionChannelEvent(
4, update.ChildPositionSequence));
Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection));
Assert.Equal((ushort)10, projection.ParentInstanceSequence);
Assert.Equal((ushort)5, projection.ChildPositionSequence);
}
[Fact]
public void RemoveAndClearDiscardPendingAndRollbackState()
{
const uint parentGuid = 0x70000130u;
const uint childGuid = 0x70000131u;
var relations = new ParentAttachmentState();
relations.AcceptCreateObjectRelation(new ParentAttachmentRelation(
parentGuid, childGuid, 1, 2, 0, 4));
relations.Enqueue(new ParentEvent.Parsed(parentGuid, childGuid, 1, 3, 9, 5));
relations.RemoveObject(parentGuid);
Assert.False(relations.TryGetProjection(childGuid, out _));
Assert.False(relations.RestoreLastAccepted(childGuid));
Assert.Empty(relations.ChildrenWaitingForParent(parentGuid));
relations.AcceptCreateObjectRelation(new ParentAttachmentRelation(
parentGuid, childGuid, 1, 2, 0, 4));
relations.Clear();
Assert.False(relations.TryGetProjection(childGuid, out _));
Assert.False(relations.RestoreLastAccepted(childGuid));
}
[Fact]
public void UnparentProjectionRetainsFresherPendingParentEvent()
{
const uint parentGuid = 0x70000160u;
const uint childGuid = 0x70000161u;
var gate = new PhysicsTimestampGate();
gate.SeedForCreateObject(4, 0, 0, 0, 0, 0, 0, 0, 3);
var relations = new ParentAttachmentState();
relations.AcceptCreateObjectRelation(new ParentAttachmentRelation(
0x70000162u, childGuid, 1, 2, 0, 4));
relations.Enqueue(new ParentEvent.Parsed(
parentGuid, childGuid, 1, 3, 9, 5));
relations.EndChildProjection(childGuid);
Assert.False(relations.TryGetProjection(childGuid, out _));
relations.Resolve(
childGuid,
_ => true,
guid => guid == parentGuid ? (ushort)9 : null,
update => gate.TryAcceptPositionChannelEvent(3, update.ChildPositionSequence));
Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection));
Assert.Equal(parentGuid, projection.ParentGuid);
Assert.Equal((ushort)5, projection.ChildPositionSequence);
}
[Fact]
public void ExactParentDeleteRetainsOnlyStrictlyFutureGenerationRelation()
{
const uint parentGuid = 0x70000170u;
const uint childGuid = 0x70000171u;
var gate = new PhysicsTimestampGate();
gate.SeedForCreateObject(4, 0, 0, 0, 0, 0, 0, 0, 3);
var relations = new ParentAttachmentState();
relations.Enqueue(new ParentEvent.Parsed(
parentGuid, childGuid, 1, 2, 9, 5));
relations.Enqueue(new ParentEvent.Parsed(
parentGuid, childGuid, 1, 3, 10, 6));
relations.DeleteGeneration(parentGuid, deletedGeneration: 9);
relations.Resolve(
childGuid,
_ => true,
guid => guid == parentGuid ? (ushort)10 : null,
update => gate.TryAcceptPositionChannelEvent(3, update.ChildPositionSequence));
Assert.True(relations.TryGetProjection(childGuid, out ParentAttachmentRelation projection));
Assert.Equal((ushort)10, projection.ParentInstanceSequence);
Assert.Equal((ushort)6, projection.ChildPositionSequence);
}
private static void Resolve(
ParentAttachmentState relations,
InboundPhysicsStateController inbound,
uint childGuid) =>
relations.Resolve(
childGuid,
guid => inbound.TryGetSnapshot(guid, out _),
guid => inbound.TryGetSnapshot(guid, out WorldSession.EntitySpawn spawn)
? spawn.InstanceSequence
: null,
update => inbound.TryApplyParent(update, out _));
private static bool Commit(
ParentAttachmentState relations,
InboundPhysicsStateController inbound,
ParentAttachmentRelation relation,
out WorldSession.EntitySpawn accepted) =>
inbound.TryCommitParent(
relation.ChildGuid,
relation.ParentGuid,
relation.ParentLocation,
relation.PlacementId,
relation.ChildPositionSequence,
out accepted)
&& relations.CommitProjection(relation);
private static WorldSession.EntitySpawn Spawn(
uint guid,
ushort instance,
ushort positionSequence)
{
var position = new CreateObject.ServerPosition(
0x0101FFFFu, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
var timestamps = new PhysicsTimestamps(
positionSequence, 1, 1, 1, 0, 1, 0, 1, instance);
var physics = new PhysicsSpawnData(
RawState: 0x408u,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
position,
0x02000001u,
Array.Empty<CreateObject.AnimPartChange>(),
Array.Empty<CreateObject.TextureChange>(),
Array.Empty<CreateObject.SubPaletteSwap>(),
null,
null,
"fixture",
null,
null,
0x09000001u,
PhysicsState: 0x408u,
InstanceSequence: instance,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: positionSequence,
Physics: physics);
}
private static WeenieData MinimalWeenie(uint guid, string name) => new(
Guid: guid,
Name: name,
Type: null,
WeenieClassId: 0,
IconId: 0,
IconOverlayId: 0,
IconUnderlayId: 0,
Effects: 0,
Value: null,
StackSize: null,
StackSizeMax: null,
Burden: null,
ContainerId: null,
WielderId: null,
ValidLocations: null,
CurrentWieldedLocation: null,
Priority: null,
ItemsCapacity: null,
ContainersCapacity: null,
Structure: null,
MaxStructure: null,
Workmanship: null);
}