Introduce LiveEntityRuntime as the canonical owner of each accepted server-object incarnation, stable local identity, timestamped state, parent relations, runtime components, and exactly-once teardown. Split logical registration from rebucketing so pending landblocks, equipment attachment, pickup re-entry, and GUID replacement reuse the same entity and effect owners. Keep canonical materialized and visible target/radar views distinct, preserve retail leave_world versus exit_world semantics, gate root simulation while cell-less, and track transitional pre-Create F754 owners through delete and session reset. Remove stale-spawn rehydration and make GpuWorldState spatial-only for live objects. Add lifecycle, generation, pending, unload, attachment, event-publication, local-ID, rollback, and effect-cleanup coverage; update architecture, milestones, memory, and the divergence register. Co-Authored-By: Codex <noreply@openai.com>
334 lines
13 KiB
C#
334 lines
13 KiB
C#
using AcDream.App.World;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Items;
|
|
|
|
namespace AcDream.App.Tests.World;
|
|
|
|
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.Null(child.Position);
|
|
Assert.Equal(parentGuid, child.ParentGuid);
|
|
Assert.Equal((ushort)5, child.PositionSequence);
|
|
}
|
|
|
|
[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)6, projection.ChildPositionSequence);
|
|
Assert.Equal((uint)3, projection.PlacementId);
|
|
relations.MarkProjected(childGuid);
|
|
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(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 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);
|
|
}
|