feat(runtime): C3c - production placement cutover: both hosts on the residence conductors (routes 1+8)
Campaign P remaining-physics-divergence, placement cutover slice C3c
(docs/plans/2026-08-02-placement-cutover.md). Both production hosts now
register every initial Create through the residence + continuation-
executor + first-entry-conductor machinery (C0-C3b):
- Graphical (route 1): RegisterEntityWithInitialResidence at Create; the
shared RuntimeFirstEntryDriveController pumps both conductors from the
placement-receipt flow; MaterializeProjection and RebucketLiveEntity
are presentation-only while a residence is ACTIVE (ExecutorCompleted is
the presentation-binding receipt); post-residence entities take the
full legacy path including the prepare_to_enter_world clock edges.
PlayerModeController attaches presentation to the Runtime-published
controller; its legacy resolve/step-heights/host-construction path is
deleted; presentation-only rollback (retail has no entry-flow rollback).
- Headless (route 8): OnSpawned registers with residence when a drive
exists; content-less sessions keep the pre-flip direct registration;
SynchronizeLocalPlayer/CreateController/ApplySetupStepHeights deleted;
prepared-collision read failure is a typed AwaitingCollisionSource
retry; far remotes outside the service window complete celless.
- RuntimeLocalPlayerMovementState.Controller setter sealed internal; all
controller mutation flows through the publication lifecycle.
Fix slices landed within this cutover, each dual-gated:
- F1: live movement-stat/server-physics application routed through the
Runtime ownership seam (post-logout ingest crash on the retired
controller eliminated; RuntimeMovementSkillProjection deleted).
- F2: login activation wedge - collision-admission prefix gate factored
out of the seal (reentrant-commit RejectedAuthority), rearm generation
identity corrected, PlayerModeAutoEntry requires the Runtime-published
controller (world reveal can no longer seal unmaterialized).
- F3: landblock-prefix 0-sentinel replaced by explicit absent-id guards;
map-corner landblocks (grid row/col 0) fully legal through admission,
park/rearm/retire, quiescence, and outdoor shadow seeds.
- F5: local-player first-entry ground contact seeded by the shared
SpawnPlacementSettler (moved App->Core) at FinalizeActivation - the
retail first-gravity-frame touch (enter_world 0x00516170 carries no
seed); the legacy unconditional force-seed is overwritten by a real
floor-found contact; airborne spawns stay airborne; outbound contact
bit verified end-to-end. Fixes the standing-cast 'You can't do that
while in the air!' rejections.
- R1 (dual-review round): login constraint leash armed at the committed
placement (HandleReceivedPosition 0x00453FD0 analog); register rows
AD-61 (settle-timing compression now covering the local player) and
AD-42 (repointed off the deleted resolve split) in this commit;
residence-conversion owner API; wire-landblock guards; drive-pending
ledger in IsConverged; route attach/detach latch; executor-drain drift
model documented + source-pinned.
Gates: Runtime 1,003, App 4,039/3 skips, Headless 79, complete solution
10,816/0 failed/4 skips (Release, -m:1); connected lifecycle/reconnect
gate PASS (logs/connected-world-gate-20260802-175401; graceful exits,
world-visible, zero airborne rejections). The nine-stop soak remains red
for the pre-existing 6b28ff99 whole-world collision-clone throughput
regression (attributed with evidence; scheduled as its own slice before
C5). Dual Opus reviews (retail-conformance + adversarial): delta PASS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
78f1eb1896
commit
529e0e9d88
68 changed files with 5977 additions and 831 deletions
|
|
@ -0,0 +1,74 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AcDream.App.Tests.World;
|
||||
|
||||
/// <summary>
|
||||
/// C3c-R1 F3 (coordinator resolution, 2026-08-02): the create-authority
|
||||
/// drift probes in the expectation-item 6/8 tests
|
||||
/// (LiveEntityHydrationControllerTests + LiveEntityCreateSupersessionRecoveryTests)
|
||||
/// hand-call <c>record.Canonical.AdvanceCreateAuthority()</c> as an HONEST
|
||||
/// MODEL of the executor drain's advance — the SOLE remaining production
|
||||
/// site that advances create authority for an existing incarnation. A
|
||||
/// nested production OnCreate can no longer produce that drift:
|
||||
/// post-residence ExistingGeneration registration is description-only
|
||||
/// (RuntimeEntityObjectLifetime gates the advance on
|
||||
/// <c>!beginInitialResidence</c>) and <c>ConsumeExecuted</c> removes the
|
||||
/// completed residence entry at Released, closing the FIFO-adoption path
|
||||
/// (empirically confirmed: the restored nested-OnCreate probe produced no
|
||||
/// drift and no CreateSupersessionRecovery). This pin flags the model as
|
||||
/// STALE if the production site ever moves or loses the advance — the
|
||||
/// item 6/8 probes must be re-derived from wherever it goes.
|
||||
/// </summary>
|
||||
public sealed class C3cR1F3DriftModelSourcePinTests
|
||||
{
|
||||
[Fact]
|
||||
public void HandCalledDriftProbe_StillModelsTheExecutorDrainAdvance()
|
||||
{
|
||||
string executor = ReadRuntimeSource(
|
||||
"Entities",
|
||||
"RuntimeInitialCreateContinuationExecutor.cs");
|
||||
|
||||
// Exactly one production advance, and it lives inside the
|
||||
// WeenieDescription drain stage the probes model.
|
||||
Assert.Single(
|
||||
Regex.Matches(executor, @"_entities\.AdvanceCreateAuthority\(")
|
||||
.Cast<Match>());
|
||||
Assert.Matches(
|
||||
new Regex(
|
||||
@"private bool ApplyWeenieDescriptionAction[\s\S]{0,6000}?"
|
||||
+ @"_entities\.AdvanceCreateAuthority\(canonical\);"),
|
||||
executor);
|
||||
|
||||
// The registration-time advance stays gated OFF the residence
|
||||
// route — the reason a nested production OnCreate cannot reach the
|
||||
// modeled drift.
|
||||
string lifetime = ReadRuntimeSource(
|
||||
"Entities",
|
||||
"RuntimeEntityObjectLifetime.cs");
|
||||
Assert.Matches(
|
||||
new Regex(
|
||||
@"if \(!beginInitialResidence\)\s*"
|
||||
+ @"Entities\.AdvanceCreateAuthority\(retained\);"),
|
||||
lifetime);
|
||||
}
|
||||
|
||||
private static string ReadRuntimeSource(params string[] relativePath)
|
||||
{
|
||||
DirectoryInfo? directory = new(AppContext.BaseDirectory);
|
||||
while (directory is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||
{
|
||||
return File.ReadAllText(Path.Combine(
|
||||
directory.FullName,
|
||||
"src",
|
||||
"AcDream.Runtime",
|
||||
Path.Combine(relativePath)));
|
||||
}
|
||||
|
||||
directory = directory.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Could not find AcDream.slnx.");
|
||||
}
|
||||
}
|
||||
|
|
@ -144,7 +144,7 @@ public sealed class DeferredLiveEntityRuntimeComponentLifecycleTests
|
|||
}
|
||||
|
||||
private static WorldSession.EntitySpawn CreateSpawn(uint guid) =>
|
||||
new(
|
||||
new WorldSession.EntitySpawn(
|
||||
Guid: guid,
|
||||
Position: null,
|
||||
SetupTableId: null,
|
||||
|
|
@ -157,5 +157,5 @@ public sealed class DeferredLiveEntityRuntimeComponentLifecycleTests
|
|||
ItemType: null,
|
||||
MotionState: null,
|
||||
MotionTableId: null,
|
||||
InstanceSequence: 1);
|
||||
InstanceSequence: 1).WithConsistentPhysics();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -370,12 +370,14 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
LiveEntityRecord record = fixture.Record;
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
var body = new PhysicsBody();
|
||||
Assert.Same(
|
||||
body,
|
||||
fixture.Runtime.GetOrCreatePhysicsBody(
|
||||
record.ServerGuid,
|
||||
_ => body));
|
||||
// C3c/C3b: the first-entry conductor constructs the canonical
|
||||
// physics body at Create (retail ACCObjectMaint::CreateObject /
|
||||
// set_description). Capture that existing body — the identity the
|
||||
// pickup/re-enter cycle must preserve — instead of seeding one.
|
||||
PhysicsBody body = fixture.Runtime.GetOrCreatePhysicsBody(
|
||||
record.ServerGuid,
|
||||
static _ => throw new InvalidOperationException(
|
||||
"The conductor-built canonical body should already exist."));
|
||||
fixture.Relationships.OnUnparentAction = _ =>
|
||||
fixture.Runtime.WithdrawLiveEntityProjection(record)
|
||||
? ChildUnparentDisposition.Completed
|
||||
|
|
@ -962,6 +964,14 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
|
||||
// Local-player records ordinarily do not rebucket from streaming
|
||||
// callbacks; an incomplete initial transaction must still recover.
|
||||
// C3c: the failed Create transaction unwound before OnCreateCore's
|
||||
// own drive pump ran, leaving the residence pending with FullCellId
|
||||
// 0 (streaming callbacks key candidates off the committed cell). In
|
||||
// production the per-frame first-entry pump completes the conductor
|
||||
// independently of the failed hydration transaction; model that
|
||||
// pump here, then let the streaming callback recover the partial
|
||||
// projection exactly as before.
|
||||
fixture.FirstEntry.DriveAll();
|
||||
fixture.Controller.OnLandblockLoaded(Cell);
|
||||
|
||||
Assert.Same(partial, record.WorldEntity);
|
||||
|
|
@ -1042,7 +1052,13 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
Assert.Equal("same generation newer", fixture.Objects.Get(Guid)!.Name);
|
||||
Assert.True(fixture.Record.InitialHydrationCompleted);
|
||||
Assert.Single(fixture.Materializer.Calls);
|
||||
Assert.Equal((ushort)2, fixture.Materializer.PositionSequences[0]);
|
||||
// C3c: the nested fresher same-generation Create is admitted into
|
||||
// the outer create's ACTIVE residence FIFO (AD-59) and its facts
|
||||
// commit at the executor drain, in array order. The single
|
||||
// materialization therefore runs from the admission-frozen seq-1
|
||||
// create; the seq-2 facts (including the name asserted above) land
|
||||
// through the drain and bind via the completion receipt.
|
||||
Assert.Equal((ushort)1, fixture.Materializer.PositionSequences[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -1114,10 +1130,16 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
Assert.Equal((ushort)1, fixture.Record.Generation);
|
||||
Assert.Equal("same generation newer", fixture.Objects.Get(Guid)!.Name);
|
||||
Assert.True(fixture.Record.InitialHydrationCompleted);
|
||||
Assert.Equal((ushort)3, fixture.Materializer.PositionSequences[^1]);
|
||||
// C3c: the nested fresher same-generation Create is admitted into
|
||||
// the replacement generation's ACTIVE residence FIFO (AD-59) and its
|
||||
// facts commit at the executor drain, in array order. The
|
||||
// materialization therefore runs from the admission-frozen seq-2
|
||||
// replacement create; the seq-3 facts (including the name asserted
|
||||
// above) land through the drain and never re-materialize.
|
||||
Assert.Equal((ushort)2, fixture.Materializer.PositionSequences[^1]);
|
||||
Assert.DoesNotContain(
|
||||
(ushort)2,
|
||||
fixture.Materializer.PositionSequences.Skip(1));
|
||||
(ushort)3,
|
||||
fixture.Materializer.PositionSequences);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -1210,11 +1232,19 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
|
||||
Assert.True(record.InitialHydrationCompleted);
|
||||
Assert.Equal("same generation newer", fixture.Objects.Get(Guid)!.Name);
|
||||
Assert.Equal([1, 1, 2], fixture.Materializer.PositionSequences);
|
||||
// C3c: a post-residence same-generation Create is description-only
|
||||
// at registration — its position churn flows through the
|
||||
// freshness-gated events tail (which is what makes RecoverProjection
|
||||
// return false above), and create authority advances only inside the
|
||||
// residence transaction. No drift means no nested
|
||||
// CreateSupersessionRecovery re-materialization: the recovery's own
|
||||
// SpatialRecovery attempt stays the last call at the original
|
||||
// installed version.
|
||||
Assert.Equal([1, 1], fixture.Materializer.PositionSequences);
|
||||
Assert.Equal(
|
||||
LiveProjectionPurpose.CreateSupersessionRecovery,
|
||||
LiveProjectionPurpose.SpatialRecovery,
|
||||
fixture.Materializer.Calls[^1].Purpose);
|
||||
Assert.Equal(2UL, fixture.Materializer.InstalledCreateIntegrationVersion);
|
||||
Assert.Equal(1UL, fixture.Materializer.InstalledCreateIntegrationVersion);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
|
@ -1236,10 +1266,23 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
if (refreshed || spawn.PositionSequence != 1)
|
||||
return;
|
||||
refreshed = true;
|
||||
fixture.Controller.OnCreate(Spawn(
|
||||
Generation: 1,
|
||||
PositionSequence: 2,
|
||||
Name: "recovery v2"));
|
||||
// C3c: a fresher same-generation CreateObject no longer advances
|
||||
// create authority at registration (its advance lands at the
|
||||
// residence drain's WeenieDescription stage). Model that exact
|
||||
// advance directly so the drift-retry machinery under test still
|
||||
// fires.
|
||||
// C3c-R1 F3 (coordinator resolution): this hand-call is an
|
||||
// honest MODEL of the executor drain's advance
|
||||
// (RuntimeInitialCreateContinuationExecutor
|
||||
// .ApplyWeenieDescriptionAction — the sole production site,
|
||||
// source-pinned by C3cR1F3DriftModelSourcePinTests). A nested
|
||||
// production OnCreate can no longer reach it here:
|
||||
// post-residence ExistingGeneration registration is
|
||||
// description-only (RuntimeEntityObjectLifetime gates the
|
||||
// advance on !beginInitialResidence, :660-665) and
|
||||
// ConsumeExecuted already removed the completed residence
|
||||
// entry, closing the FIFO-adoption path.
|
||||
record.Canonical.AdvanceCreateAuthority();
|
||||
};
|
||||
fixture.Materializer.ThrowAfterMaterializePurposeOnce =
|
||||
LiveProjectionPurpose.CreateSupersessionRecovery;
|
||||
|
|
@ -1270,8 +1313,12 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
|
||||
Assert.False(record.CreateProjectionSynchronizationPending);
|
||||
Assert.Same(retained, record.WorldEntity);
|
||||
// C3c: the retry retransmit (a post-residence same-generation
|
||||
// Create) no longer advances create authority itself, so both retry
|
||||
// paths install the drift probe's version (2), not a
|
||||
// retransmit-advanced 3.
|
||||
Assert.Equal(
|
||||
retryFromLandblock ? 2UL : 3UL,
|
||||
2UL,
|
||||
fixture.Materializer.InstalledCreateIntegrationVersion);
|
||||
Assert.Equal(
|
||||
LiveProjectionPurpose.CreateSupersessionRecovery,
|
||||
|
|
@ -1357,10 +1404,23 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
if (refreshed || spawn.PositionSequence != 1)
|
||||
return;
|
||||
refreshed = true;
|
||||
fixture.Controller.OnCreate(Spawn(
|
||||
Generation: 1,
|
||||
PositionSequence: 2,
|
||||
Name: "ready v2"));
|
||||
// C3c: a fresher same-generation CreateObject no longer advances
|
||||
// create authority at registration (its advance lands at the
|
||||
// residence drain's WeenieDescription stage). Model that exact
|
||||
// advance directly so the drift-retry machinery under test still
|
||||
// fires.
|
||||
// C3c-R1 F3 (coordinator resolution): this hand-call is an
|
||||
// honest MODEL of the executor drain's advance
|
||||
// (RuntimeInitialCreateContinuationExecutor
|
||||
// .ApplyWeenieDescriptionAction — the sole production site,
|
||||
// source-pinned by C3cR1F3DriftModelSourcePinTests). A nested
|
||||
// production OnCreate can no longer reach it here:
|
||||
// post-residence ExistingGeneration registration is
|
||||
// description-only (RuntimeEntityObjectLifetime gates the
|
||||
// advance on !beginInitialResidence, :660-665) and
|
||||
// ConsumeExecuted already removed the completed residence
|
||||
// entry, closing the FIFO-adoption path.
|
||||
record.Canonical.AdvanceCreateAuthority();
|
||||
};
|
||||
fixture.Ready.FailPublishCount = 1;
|
||||
|
||||
|
|
@ -1486,6 +1546,12 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
{
|
||||
const uint parentGuid = 0x70000002u;
|
||||
using var fixture = new Fixture(originKnown: true);
|
||||
// C3c: a Create whose parent is not addressable is now queued under
|
||||
// the parent's GUID (retail QueueBlobForObject) instead of applying
|
||||
// immediately. Register the parent so the nested parented Create
|
||||
// routes exactly as before.
|
||||
fixture.Runtime.RegisterLiveEntity(
|
||||
Spawn(Generation: 1, PositionSequence: 1) with { Guid = parentGuid });
|
||||
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
|
||||
LiveEntityRecord record = fixture.Record;
|
||||
WorldEntity retained = record.WorldEntity!;
|
||||
|
|
@ -1522,6 +1588,17 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
record,
|
||||
positionVersion));
|
||||
Assert.True(fixture.Runtime.WithdrawLiveEntityProjection(record));
|
||||
// C3c: mirror the production relationship owner
|
||||
// (EquippedChildRenderController.TryAttach), which converts a
|
||||
// residence-managed child's sticky residence to LegacyImmediate
|
||||
// at the world -> attached kind transition.
|
||||
if (record.MaterializationResidence is
|
||||
AcDream.App.World.LiveEntityMaterializationResidence
|
||||
.AwaitRuntimePlacement)
|
||||
{
|
||||
record.MaterializationResidence = AcDream.App.World
|
||||
.LiveEntityMaterializationResidence.LegacyImmediate;
|
||||
}
|
||||
WorldEntity? attached = fixture.Runtime.MaterializeLiveEntity(
|
||||
Guid,
|
||||
Cell,
|
||||
|
|
@ -1540,6 +1617,12 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
fixture.Controller.OnCreate(CelllessSpawn(
|
||||
PositionSequence: 2,
|
||||
parentGuid));
|
||||
// C3c: a post-residence same-generation Create no longer
|
||||
// advances create authority at registration (the advance lands
|
||||
// at a residence drain's WeenieDescription stage). Model that
|
||||
// advance directly so the supersession-recovery machinery under
|
||||
// test still fires and completes at the attached-ready boundary.
|
||||
record.Canonical.AdvanceCreateAuthority();
|
||||
};
|
||||
|
||||
Assert.False(fixture.Controller.RecoverProjection(
|
||||
|
|
@ -1575,6 +1658,25 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
using var fixture = new Fixture(
|
||||
originKnown: true,
|
||||
playerGuid: retryFromLandblock ? Guid : 0u);
|
||||
// C3c: a Create whose parent is not addressable is now queued under
|
||||
// the parent's GUID (retail QueueBlobForObject) instead of applying
|
||||
// immediately. Register the parent so the initial parented Create
|
||||
// routes exactly as before — placed in a DIFFERENT landblock so this
|
||||
// scaffolding identity is not itself a candidate for the recovered
|
||||
// landblock's projection sweep (the child's RegisterCount assertions
|
||||
// count only the child's resources).
|
||||
WorldSession.EntitySpawn parentSpawn =
|
||||
Spawn(Generation: 1, PositionSequence: 1) with { Guid = parentGuid };
|
||||
var parentPosition = new CreateObject.ServerPosition(
|
||||
0x01020001u, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
|
||||
fixture.Runtime.RegisterLiveEntity(parentSpawn with
|
||||
{
|
||||
Position = parentPosition,
|
||||
Physics = parentSpawn.Physics!.Value with
|
||||
{
|
||||
Position = parentPosition,
|
||||
},
|
||||
});
|
||||
fixture.Ready.FailPublishCount = 2;
|
||||
fixture.Network.ApplyAction = events =>
|
||||
{
|
||||
|
|
@ -1659,8 +1761,20 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
if (!replaced && replacementStage == stage)
|
||||
{
|
||||
replaced = true;
|
||||
fixture.Runtime.RegisterLiveEntity(
|
||||
Spawn(Generation: 1, PositionSequence: 2));
|
||||
// C3c: a fresher same-generation CreateObject no longer
|
||||
// advances create authority at registration — its advance
|
||||
// lands at the residence drain's WeenieDescription stage.
|
||||
// Model that exact advance directly so the between-stage
|
||||
// revalidation guard stays covered.
|
||||
// C3c-R1 F3: honest MODEL of the executor drain's advance
|
||||
// (ApplyWeenieDescriptionAction — the sole production site,
|
||||
// source-pinned by C3cR1F3DriftModelSourcePinTests); a
|
||||
// nested production OnCreate can no longer reach it —
|
||||
// post-residence registration is description-only
|
||||
// (RuntimeEntityObjectLifetime :660-665,
|
||||
// !beginInitialResidence gate) and ConsumeExecuted already
|
||||
// removed the completed residence entry.
|
||||
expected.Canonical.AdvanceCreateAuthority();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1768,8 +1882,18 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
var projection = new RecordingLiveProjectionSink(
|
||||
_ =>
|
||||
{
|
||||
fixture.Runtime.RegisterLiveEntity(
|
||||
Spawn(Generation: 1, PositionSequence: 2));
|
||||
// C3c: a fresher same-generation CreateObject's authority
|
||||
// advance now lands at the residence drain's
|
||||
// WeenieDescription stage; model that advance directly.
|
||||
// C3c-R1 F3: honest MODEL of the executor drain's advance
|
||||
// (ApplyWeenieDescriptionAction — the sole production site,
|
||||
// source-pinned by C3cR1F3DriftModelSourcePinTests); a
|
||||
// nested production OnCreate can no longer reach it —
|
||||
// post-residence registration is description-only
|
||||
// (RuntimeEntityObjectLifetime :660-665,
|
||||
// !beginInitialResidence gate) and ConsumeExecuted already
|
||||
// removed the completed residence entry.
|
||||
fixture.Record.Canonical.AdvanceCreateAuthority();
|
||||
return true;
|
||||
});
|
||||
var publisher = new LiveEntityReadyPublisher(
|
||||
|
|
@ -1792,10 +1916,19 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
WorldEntity entity = record.WorldEntity!;
|
||||
ulong capturedCreateIntegrationVersion = record.CreateIntegrationVersion;
|
||||
|
||||
// Models ProjectionPoseReady synchronously accepting a fresher
|
||||
// same-generation CreateObject before EntityReady is emitted.
|
||||
fixture.Runtime.RegisterLiveEntity(
|
||||
Spawn(Generation: 1, PositionSequence: 2));
|
||||
// Models ProjectionPoseReady synchronously observing a fresher
|
||||
// same-generation CreateObject's authority advance before
|
||||
// EntityReady is emitted. C3c: that advance now lands at the
|
||||
// residence drain's WeenieDescription stage
|
||||
// (AdvanceCreateAuthority), not at registration; model it directly.
|
||||
// C3c-R1 F3: honest MODEL of the executor drain's advance
|
||||
// (ApplyWeenieDescriptionAction — the sole production site,
|
||||
// source-pinned by C3cR1F3DriftModelSourcePinTests); a nested
|
||||
// production OnCreate can no longer reach it — post-residence
|
||||
// registration is description-only (RuntimeEntityObjectLifetime
|
||||
// :660-665, !beginInitialResidence gate) and ConsumeExecuted
|
||||
// already removed the completed residence entry.
|
||||
record.Canonical.AdvanceCreateAuthority();
|
||||
bool published = false;
|
||||
|
||||
Assert.False(EquippedChildRenderController.PublishEntityReadyExact(
|
||||
|
|
@ -1937,6 +2070,55 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
uint playerGuid = 0u)
|
||||
{
|
||||
Resources = resources ?? new RecordingResources();
|
||||
// C3c: initial-Create registration begins the canonical
|
||||
// residence, whose admission requires a live generation; the
|
||||
// fixture also commits the wire landblock's collision generation
|
||||
// and wires the production first-entry drive pump so each Create
|
||||
// transaction completes its conductor synchronously, exactly
|
||||
// like the composed graphical host.
|
||||
EntityObjects.BindEventContext(
|
||||
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
|
||||
static () => 1UL);
|
||||
EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
||||
Cell & 0xFFFF0000u, 1UL);
|
||||
EntityObjects.Physics.Engine.AddLandblock(
|
||||
Cell & 0xFFFF0000u,
|
||||
new AcDream.Core.Physics.TerrainSurface(
|
||||
new byte[81], new float[256]),
|
||||
Array.Empty<AcDream.Core.Physics.CellSurface>(),
|
||||
Array.Empty<AcDream.Core.Physics.PortalPlane>(),
|
||||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f);
|
||||
EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
||||
Cell & 0xFFFF0000u, 1UL, ready: true);
|
||||
Movement = new AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState();
|
||||
IdentityState = new AcDream.Runtime.Gameplay.RuntimeLocalPlayerIdentityState();
|
||||
var publication = new AcDream.Runtime.Gameplay
|
||||
.RuntimeLocalPlayerPhysicsPublicationState(
|
||||
EntityObjects.Entities,
|
||||
EntityObjects.Physics,
|
||||
Movement,
|
||||
IdentityState);
|
||||
Movement.AttachPhysicsPublication(publication);
|
||||
EntityObjects.LocalPlayerFirstEntry.BindPublication(publication);
|
||||
IdentityState.ServerGuid = playerGuid;
|
||||
FirstEntry = new AcDream.Runtime.Session.RuntimeFirstEntryDriveController(
|
||||
EntityObjects,
|
||||
new AcDream.Runtime.GameRuntimeClock(),
|
||||
new HydrationNullCollisionSource(),
|
||||
() => AcDream.Runtime.Gameplay.PlayerMovementConstructionOptions.Fallback,
|
||||
static _ => new AcDream.Runtime.Gameplay
|
||||
.RuntimeLocalPlayerPhysicsActivationPreparation(
|
||||
0.48f,
|
||||
1.835f,
|
||||
AcDream.Runtime.Gameplay
|
||||
.RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
|
||||
// C3c: the real per-session placement subscription — without it
|
||||
// the first entity's unacknowledged ExecutorCompleted receipt
|
||||
// wedges the one ordered FIFO and every later Create's conductor
|
||||
// yields AwaitingReceiptAcknowledgement forever. Ack rules mirror
|
||||
// production: Discard/ExecutorCompleted acknowledge-only;
|
||||
// Place/Withdraw stay at the head for the conductor machinery.
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(new LoadedLandblock(
|
||||
0x0101FFFFu,
|
||||
|
|
@ -1947,6 +2129,11 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
Resources,
|
||||
Teardown,
|
||||
EntityObjects);
|
||||
_placements = new AcDream.Runtime.Physics
|
||||
.RuntimePlacementProjectionSubscription(
|
||||
EntityObjects.Placements,
|
||||
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
|
||||
new FixturePlacementSink(Runtime));
|
||||
Materializer = new RecordingMaterializer(Runtime, Operations);
|
||||
Relationships = new RecordingRelationships(Operations);
|
||||
Ready = new RecordingReadyPublisher(Operations);
|
||||
|
|
@ -1988,7 +2175,90 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
Timestamps,
|
||||
identity,
|
||||
deletion,
|
||||
Dormant);
|
||||
Dormant,
|
||||
firstEntry: FirstEntry);
|
||||
}
|
||||
|
||||
public AcDream.Runtime.Session.RuntimeFirstEntryDriveController FirstEntry { get; }
|
||||
private readonly AcDream.Runtime.Physics
|
||||
.RuntimePlacementProjectionSubscription _placements;
|
||||
|
||||
private sealed class FixturePlacementSink(LiveEntityRuntime runtime)
|
||||
: AcDream.Runtime.Physics.IRuntimePlacementProjectionSink
|
||||
{
|
||||
public bool TryApply(
|
||||
in AcDream.Runtime.Physics.RuntimePlacementProjectionSnapshot projection)
|
||||
{
|
||||
if (projection.Kind is AcDream.Runtime.Physics
|
||||
.RuntimePlacementProjectionKind.Discard)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (projection.Kind is AcDream.Runtime.Physics
|
||||
.RuntimePlacementProjectionKind.ExecutorCompleted)
|
||||
{
|
||||
return projection.Token.ExactCellId == 0u
|
||||
|| runtime.TryApplyInitialCreateCompletionPresentation(
|
||||
in projection);
|
||||
}
|
||||
return !runtime.HasActiveInitialCreateResidence(
|
||||
projection.Token.Entity)
|
||||
&& runtime.TryApplyRuntimePlacementProjection(in projection);
|
||||
}
|
||||
}
|
||||
public AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState Movement { get; }
|
||||
public AcDream.Runtime.Gameplay.RuntimeLocalPlayerIdentityState IdentityState { get; }
|
||||
|
||||
private sealed class HydrationNullCollisionSource
|
||||
: AcDream.Content.IPreparedCollisionSource
|
||||
{
|
||||
public AcDream.Content.PreparedAssetPresence ProbeCollision(
|
||||
AcDream.Content.Pak.PakAssetType type,
|
||||
uint sourceFileId) =>
|
||||
AcDream.Content.PreparedAssetPresence.Available;
|
||||
|
||||
public AcDream.Content.PreparedCollisionReadResult<
|
||||
AcDream.Core.Physics.FlatSetupCollision> ReadSetupCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
AcDream.Content.PreparedCollisionReadResult<
|
||||
AcDream.Core.Physics.FlatSetupCollision>.Loaded(
|
||||
new AcDream.Core.Physics.FlatSetupCollision(
|
||||
System.Collections.Immutable.ImmutableArray<
|
||||
AcDream.Core.Physics.FlatCollisionCylinder>.Empty,
|
||||
[new AcDream.Core.Physics.FlatCollisionSphere(
|
||||
System.Numerics.Vector3.Zero, 0.48f)],
|
||||
height: 0f,
|
||||
radius: 0f,
|
||||
stepUpHeight: 0.4f,
|
||||
stepDownHeight: 0.4f));
|
||||
|
||||
public AcDream.Content.PreparedCollisionReadResult<
|
||||
AcDream.Core.Physics.FlatGfxObjCollisionAsset>
|
||||
ReadGfxObjCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public AcDream.Content.PreparedCollisionReadResult<
|
||||
AcDream.Core.Physics.FlatCellStructureCollisionAsset>
|
||||
ReadCellStructureCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public AcDream.Content.PreparedCollisionReadResult<
|
||||
AcDream.Core.Physics.FlatEnvCellTopology> ReadEnvCellTopology(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public AcDream.Content.PreparedCollisionSourceStats CollisionStats =>
|
||||
default;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public LiveEntityRecord Record
|
||||
|
|
@ -2165,7 +2435,34 @@ public sealed class LiveEntityHydrationControllerTests
|
|||
},
|
||||
LiveEntityProjectionKind.World,
|
||||
initializeProjection: null,
|
||||
out LiveEntityRecord? expectedRecord);
|
||||
out LiveEntityRecord? expectedRecord,
|
||||
// C3c: mirror the production materializer
|
||||
// (DatLiveEntityProjectionMaterializer.MaterializeProjection),
|
||||
// which materializes route-1 world creates residence-managed.
|
||||
// The legacy-immediate default would commit the wire cell
|
||||
// out-of-band and retire the fresh residence lease before the
|
||||
// fixture's drive pump ever ran (diagnosed RejectedToken).
|
||||
AcDream.App.World.LiveEntityMaterializationResidence
|
||||
.AwaitRuntimePlacement);
|
||||
// C3c: mirror the production materializer's self-projection
|
||||
// branch — when the residence-driven placement already committed
|
||||
// (or a legacy post-residence path committed the cell) before
|
||||
// this sidecar could exist, its completion receipt is gone, so
|
||||
// presentation self-projects from the committed canonical state
|
||||
// through the presentation-only bucket path.
|
||||
if (entity is not null
|
||||
&& expectedRecord is not null
|
||||
&& runtime.IsCurrentCreateIntegration(
|
||||
expectedCanonical,
|
||||
expectedCreateIntegrationVersion)
|
||||
&& expectedCanonical.FullCellId != 0u
|
||||
&& !runtime.HasActiveInitialCreateResidence(expectedCanonical)
|
||||
&& !runtime.RebucketLiveEntity(
|
||||
canonicalSpawn.Guid,
|
||||
expectedCanonical.FullCellId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (ThrowAfterMaterializePurposeOnce == purpose)
|
||||
{
|
||||
ThrowAfterMaterializePurposeOnce = null;
|
||||
|
|
|
|||
|
|
@ -423,6 +423,11 @@ public sealed class LiveEntityLifecycleStressTests
|
|||
canAdvanceOwner: ownerId => _effects?.CanAdvanceOwner(ownerId) ?? true);
|
||||
|
||||
EntityObjects = new RuntimeEntityObjectLifetime(Engine);
|
||||
// C3c: initial-Create registration begins the canonical residence,
|
||||
// whose admission requires a live session generation.
|
||||
EntityObjects.BindEventContext(
|
||||
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
|
||||
static () => 1UL);
|
||||
Runtime = new LiveEntityRuntime(
|
||||
Spatial,
|
||||
new DelegateLiveEntityResourceLifecycle(
|
||||
|
|
|
|||
|
|
@ -639,6 +639,9 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
|||
teardown);
|
||||
|
||||
private static WorldSession.EntitySpawn Spawn(uint guid, ushort instance) =>
|
||||
// C3c: residence admission requires the flattened identity fields to
|
||||
// agree with a nested PhysicsDesc block; a bare logical fixture
|
||||
// carries the minimal consistent one (instance timestamp only).
|
||||
new(
|
||||
Guid: guid,
|
||||
Position: null,
|
||||
|
|
@ -652,7 +655,37 @@ public sealed class LiveEntityPhysicsHostOwnershipTests
|
|||
ItemType: null,
|
||||
MotionState: null,
|
||||
MotionTableId: null,
|
||||
InstanceSequence: instance);
|
||||
InstanceSequence: instance,
|
||||
Physics: new PhysicsSpawnData(
|
||||
RawState: 0u,
|
||||
Position: null,
|
||||
Movement: null,
|
||||
AnimationFrame: null,
|
||||
SetupTableId: null,
|
||||
MotionTableId: null,
|
||||
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: new PhysicsTimestamps(
|
||||
Position: 0,
|
||||
Movement: 0,
|
||||
State: 0,
|
||||
Vector: 0,
|
||||
Teleport: 0,
|
||||
ServerControlledMove: 0,
|
||||
ForcePosition: 0,
|
||||
ObjDesc: 0,
|
||||
Instance: instance)));
|
||||
|
||||
private static AcDream.Core.World.WorldEntity Entity(
|
||||
uint localId,
|
||||
|
|
|
|||
|
|
@ -733,7 +733,10 @@ public sealed class LiveEntityPresentationControllerTests
|
|||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
0x01010001u, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
|
||||
var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1);
|
||||
// C3c: residence admission requires the nested block's Instance
|
||||
// timestamp to agree with the flattened InstanceSequence.
|
||||
var timestamps = new PhysicsTimestamps(
|
||||
1, 1, 1, 1, 0, 1, 0, 1, instanceSequence);
|
||||
var physics = new PhysicsSpawnData(
|
||||
RawState: (uint)state,
|
||||
Position: position,
|
||||
|
|
|
|||
|
|
@ -247,6 +247,9 @@ public sealed class LiveEntityProjectionWithdrawalControllerTests
|
|||
MotionState: null,
|
||||
MotionTableId: null,
|
||||
InstanceSequence: instance);
|
||||
// C3c: residence admission requires the flattened parser
|
||||
// projections to agree with a nested PhysicsDesc block.
|
||||
spawn = spawn.WithConsistentPhysics();
|
||||
LiveEntityRecord record = Live.RegisterAndMaterializeProjection(
|
||||
spawn,
|
||||
id => new WorldEntity
|
||||
|
|
|
|||
|
|
@ -826,13 +826,22 @@ public sealed class LiveEntityRuntimeTests
|
|||
{
|
||||
const uint parentGuid = 0x70000020u;
|
||||
const uint childGuid = 0x70000021u;
|
||||
var runtime = LiveEntityRuntimeFixture.Create(new GpuWorldState(), new RecordingResources());
|
||||
// C3c: initial residences defer wire applies into their FIFO until a
|
||||
// host pump drives the conductors; use the driven fixture and pump
|
||||
// after each Create so the parent-event tail commits as before.
|
||||
LiveEntityRuntimeFixture.DrivenLiveEntityRuntime driven =
|
||||
LiveEntityRuntimeFixture.CreateDriven(
|
||||
new GpuWorldState(),
|
||||
new RecordingResources());
|
||||
LiveEntityRuntime runtime = driven.Runtime;
|
||||
runtime.RegisterLiveEntity(Spawn(parentGuid, 9, 1, 0x01010001u));
|
||||
driven.Pump();
|
||||
runtime.ParentAttachments.Enqueue(new ParentEvent.Parsed(
|
||||
parentGuid, childGuid, 1, 2, 9, 5));
|
||||
ResolveParent(runtime, childGuid);
|
||||
|
||||
runtime.RegisterLiveEntity(Spawn(childGuid, 3, 4, 0x01010001u));
|
||||
driven.Pump();
|
||||
ResolveParent(runtime, childGuid);
|
||||
|
||||
Assert.True(runtime.ParentAttachments.TryGetProjection(
|
||||
|
|
@ -1261,6 +1270,143 @@ public sealed class LiveEntityRuntimeTests
|
|||
Assert.Equal(0.0, clock.PendingSeconds, 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C3c-R1 review R2: the presentation-only rebucket shortcut is scoped
|
||||
/// to the ACTIVE initial-create residence (where the public API is
|
||||
/// suppressed outright — the conductor's completion receipt is the only
|
||||
/// presentation channel). A RETIRED-residence entity's sticky
|
||||
/// MaterializationResidence must NOT keep it on the shortcut: the
|
||||
/// unflipped legacy update routes (network position/state, teleports,
|
||||
/// streaming reprojection, hydration recovery — all callers of this one
|
||||
/// public RebucketLiveEntity chokepoint) are the position authority
|
||||
/// again, so post-residence moves take the FULL legacy branch:
|
||||
/// CommitRebucket writes the canonical cell and retail's
|
||||
/// prepare_to_enter_world (0x00511FA0) clock rebase runs on every
|
||||
/// root-workset membership edge.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void PostResidenceRebucket_TakesTheFullLegacyPathIncludingTheClockEdge()
|
||||
{
|
||||
const uint guid = 0x7000004Au;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
spatial.AddLandblock(EmptyLandblock(0x0102FFFFu));
|
||||
LiveEntityRuntimeFixture.DrivenLiveEntityRuntime driven =
|
||||
LiveEntityRuntimeFixture.CreateDriven(
|
||||
spatial,
|
||||
new RecordingResources());
|
||||
LiveEntityRuntime runtime = driven.Runtime;
|
||||
RuntimeEntityRecord canonical =
|
||||
Assert.IsType<RuntimeEntityRecord>(runtime.RegisterLiveEntity(
|
||||
Spawn(guid, 1, 1, 0x01010001u)).Canonical);
|
||||
runtime.MaterializeLiveEntity(
|
||||
canonical,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid),
|
||||
LiveEntityProjectionKind.World,
|
||||
initializeProjection: null,
|
||||
out _,
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement);
|
||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||
|
||||
// ACTIVE residence: the public API stays suppressed (the completion
|
||||
// receipt is the entity's first world-visible moment) and no legacy
|
||||
// cell commit can race the conductor's pending placement.
|
||||
Assert.True(runtime.HasActiveInitialCreateResidence(canonical));
|
||||
Assert.False(runtime.RebucketLiveEntity(guid, 0x01020001u));
|
||||
Assert.Equal(0u, canonical.FullCellId);
|
||||
// C3c-R1 review F5: the tracked-but-undriven entry is visible in
|
||||
// the entity-object ownership ledger while it awaits its pump.
|
||||
Assert.Equal(
|
||||
1,
|
||||
driven.Lifetime.CaptureOwnership().FirstEntryDrivePendingCount);
|
||||
|
||||
driven.Pump();
|
||||
Assert.False(runtime.HasActiveInitialCreateResidence(canonical));
|
||||
Assert.Equal(
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement,
|
||||
record.MaterializationResidence);
|
||||
Assert.Equal(0x01010001u, canonical.FullCellId);
|
||||
|
||||
// C3c-R1 review F5: the drive's tracked entries fold into the
|
||||
// entity-object ownership ledger — one pending entry while the
|
||||
// residence awaited its pump, zero after.
|
||||
Assert.Equal(
|
||||
0,
|
||||
driven.Lifetime.CaptureOwnership().FirstEntryDrivePendingCount);
|
||||
|
||||
// RETIRED residence, loaded-to-loaded: full legacy branch commits
|
||||
// the canonical cell (the presentation-only shortcut never did) and
|
||||
// preserves the running clock.
|
||||
RetailObjectQuantumClock clock = record.ObjectClock;
|
||||
Assert.Equal(0, clock.Advance(0.02).Count);
|
||||
Assert.True(runtime.RebucketLiveEntity(guid, 0x01020001u));
|
||||
Assert.Equal(0x01020001u, canonical.FullCellId);
|
||||
Assert.Same(clock, record.ObjectClock);
|
||||
Assert.True(clock.IsActive);
|
||||
Assert.Equal(0.02, clock.PendingSeconds, 8);
|
||||
|
||||
// Membership edge into a pending bucket suspends the clock; the
|
||||
// pending drain's reentry rebases it for enter-world — the retail
|
||||
// prepare_to_enter_world edge the shortcut skipped.
|
||||
Assert.True(runtime.RebucketLiveEntity(guid, 0x02020001u));
|
||||
Assert.Equal(0x02020001u, canonical.FullCellId);
|
||||
Assert.False(clock.IsActive);
|
||||
spatial.AddLandblock(EmptyLandblock(0x0202FFFFu));
|
||||
Assert.True(clock.IsActive);
|
||||
Assert.Equal(0.0, clock.PendingSeconds, 8);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C3c-R1 review F1: converting the sticky residence-managed
|
||||
/// presentation kind to LegacyImmediate (the equipped-child
|
||||
/// world→attached transition) is the owner's explicit API — it refuses
|
||||
/// while the initial-create residence lease is still active, because an
|
||||
/// attached materialization would otherwise race the conductor's
|
||||
/// pending placement.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ResidenceConversionToLegacyImmediate_RefusesWhileTheResidenceIsActive()
|
||||
{
|
||||
const uint guid = 0x7000004Bu;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
LiveEntityRuntimeFixture.DrivenLiveEntityRuntime driven =
|
||||
LiveEntityRuntimeFixture.CreateDriven(
|
||||
spatial,
|
||||
new RecordingResources());
|
||||
LiveEntityRuntime runtime = driven.Runtime;
|
||||
RuntimeEntityRecord canonical =
|
||||
Assert.IsType<RuntimeEntityRecord>(runtime.RegisterLiveEntity(
|
||||
Spawn(guid, 1, 1, 0x01010001u)).Canonical);
|
||||
runtime.MaterializeLiveEntity(
|
||||
canonical,
|
||||
0x01010001u,
|
||||
id => Entity(id, guid),
|
||||
LiveEntityProjectionKind.World,
|
||||
initializeProjection: null,
|
||||
out _,
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement);
|
||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord record));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
runtime.ConvertMaterializationResidenceToLegacyImmediate(record));
|
||||
Assert.Equal(
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement,
|
||||
record.MaterializationResidence);
|
||||
|
||||
driven.Pump();
|
||||
runtime.ConvertMaterializationResidenceToLegacyImmediate(record);
|
||||
Assert.Equal(
|
||||
LiveEntityMaterializationResidence.LegacyImmediate,
|
||||
record.MaterializationResidence);
|
||||
// Idempotent once converted (and a no-op for legacy records).
|
||||
runtime.ConvertMaterializationResidenceToLegacyImmediate(record);
|
||||
Assert.Equal(
|
||||
LiveEntityMaterializationResidence.LegacyImmediate,
|
||||
record.MaterializationResidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InitiallyVisibleStaticObject_RebasesWithoutBecomingActive()
|
||||
{
|
||||
|
|
@ -1428,10 +1574,27 @@ public sealed class LiveEntityRuntimeTests
|
|||
{
|
||||
const uint stateBeforeBindGuid = 0x70000037u;
|
||||
const uint bindBeforeStateGuid = 0x70000038u;
|
||||
var runtime = LiveEntityRuntimeFixture.Create(new GpuWorldState(), new RecordingResources());
|
||||
runtime.RegisterLiveEntity(Spawn(stateBeforeBindGuid, 1, 1, 0x01010001u));
|
||||
runtime.RegisterLiveEntity(Spawn(bindBeforeStateGuid, 1, 1, 0x01010001u));
|
||||
// C3c: initial residences defer wire applies into their FIFO until a
|
||||
// host pump drives the conductors.
|
||||
LiveEntityRuntimeFixture.DrivenLiveEntityRuntime driven =
|
||||
LiveEntityRuntimeFixture.CreateDriven(
|
||||
new GpuWorldState(),
|
||||
new RecordingResources());
|
||||
LiveEntityRuntime runtime = driven.Runtime;
|
||||
RuntimeEntityRecord stateBeforeBindCanonical =
|
||||
Assert.IsType<RuntimeEntityRecord>(runtime.RegisterLiveEntity(
|
||||
Spawn(stateBeforeBindGuid, 1, 1, 0x01010001u)).Canonical);
|
||||
RuntimeEntityRecord bindBeforeStateCanonical =
|
||||
Assert.IsType<RuntimeEntityRecord>(runtime.RegisterLiveEntity(
|
||||
Spawn(bindBeforeStateGuid, 1, 1, 0x01010001u)).Canonical);
|
||||
|
||||
// C3c/C3b: the first-entry conductor constructs the canonical body
|
||||
// at Create (never-clobber: a fixture can no longer seed a
|
||||
// replacement RemoteMotionRuntime over it). "Arrival order" is now
|
||||
// SetState-before-the-drain (FIFO'd into the pending residence,
|
||||
// applied against the conductor-built body at Execute) versus
|
||||
// SetState-after-completion (legacy immediate apply). Both must
|
||||
// leave the canonical body's state synchronized.
|
||||
PhysicsStateFlags firstState = PhysicsStateFlags.Hidden
|
||||
| PhysicsStateFlags.Gravity
|
||||
| PhysicsStateFlags.ReportCollisions;
|
||||
|
|
@ -1439,18 +1602,30 @@ public sealed class LiveEntityRuntimeTests
|
|||
new SetState.Parsed(stateBeforeBindGuid, (uint)firstState, 1, 2),
|
||||
out _));
|
||||
runtime.MaterializeLiveEntity(
|
||||
stateBeforeBindGuid,
|
||||
stateBeforeBindCanonical,
|
||||
0x01010001u,
|
||||
id => Entity(id, stateBeforeBindGuid));
|
||||
var lateBody = new RemoteMotionRuntime();
|
||||
runtime.SetRemoteMotionRuntime(stateBeforeBindGuid, lateBody);
|
||||
|
||||
id => Entity(id, stateBeforeBindGuid),
|
||||
LiveEntityProjectionKind.World,
|
||||
initializeProjection: null,
|
||||
out _,
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement);
|
||||
runtime.MaterializeLiveEntity(
|
||||
bindBeforeStateGuid,
|
||||
bindBeforeStateCanonical,
|
||||
0x01010001u,
|
||||
id => Entity(id, bindBeforeStateGuid));
|
||||
var earlyBody = new RemoteMotionRuntime();
|
||||
runtime.SetRemoteMotionRuntime(bindBeforeStateGuid, earlyBody);
|
||||
id => Entity(id, bindBeforeStateGuid),
|
||||
LiveEntityProjectionKind.World,
|
||||
initializeProjection: null,
|
||||
out _,
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement);
|
||||
driven.Pump();
|
||||
PhysicsBody lateBody = runtime.GetOrCreatePhysicsBody(
|
||||
stateBeforeBindGuid,
|
||||
static _ => throw new InvalidOperationException(
|
||||
"The conductor-built canonical body should already exist."));
|
||||
PhysicsBody earlyBody = runtime.GetOrCreatePhysicsBody(
|
||||
bindBeforeStateGuid,
|
||||
static _ => throw new InvalidOperationException(
|
||||
"The conductor-built canonical body should already exist."));
|
||||
PhysicsStateFlags secondState = PhysicsStateFlags.Static
|
||||
| PhysicsStateFlags.Ethereal
|
||||
| PhysicsStateFlags.NoDraw;
|
||||
|
|
@ -1460,8 +1635,8 @@ public sealed class LiveEntityRuntimeTests
|
|||
|
||||
Assert.Equal((firstState & ~PhysicsStateFlags.ReportCollisions)
|
||||
| PhysicsStateFlags.IgnoreCollisions,
|
||||
lateBody.Body.State);
|
||||
Assert.Equal(secondState, earlyBody.Body.State);
|
||||
lateBody.State);
|
||||
Assert.Equal(secondState, earlyBody.State);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -1573,9 +1748,17 @@ public sealed class LiveEntityRuntimeTests
|
|||
public void PositionAfterPickup_RequiresTeleportHookEvenWithEqualTeleportStamp()
|
||||
{
|
||||
const uint guid = 0x70000043u;
|
||||
var runtime = LiveEntityRuntimeFixture.Create(new GpuWorldState(), new RecordingResources());
|
||||
// C3c: initial residences defer wire applies into their FIFO until a
|
||||
// host pump drives the conductors; use the driven fixture and pump
|
||||
// after the Create so the pickup/position tail flows as before.
|
||||
LiveEntityRuntimeFixture.DrivenLiveEntityRuntime driven =
|
||||
LiveEntityRuntimeFixture.CreateDriven(
|
||||
new GpuWorldState(),
|
||||
new RecordingResources());
|
||||
LiveEntityRuntime runtime = driven.Runtime;
|
||||
WorldSession.EntitySpawn spawn = Spawn(guid, 1, 1, 0x01010001u);
|
||||
runtime.RegisterLiveEntity(spawn);
|
||||
driven.Pump();
|
||||
Assert.True(runtime.TryApplyPickup(
|
||||
new PickupEvent.Parsed(guid, 1, 2),
|
||||
out _));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,743 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Plugins;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.Runtime.Entities;
|
||||
using AcDream.Runtime.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Tests.World;
|
||||
|
||||
/// <summary>
|
||||
/// C3c contract integration tests: the flipped graphical host wiring driven
|
||||
/// end-to-end — registration through <see cref="LiveEntityHydrationController"/>,
|
||||
/// the REAL <see cref="RuntimePlacementPresentationSink"/> behind the REAL
|
||||
/// <see cref="AcDream.Runtime.Physics.RuntimePlacementProjectionSubscription"/>,
|
||||
/// and the production
|
||||
/// <see cref="AcDream.Runtime.Session.RuntimeFirstEntryDriveController"/> pump.
|
||||
/// No conductor is ever hand-called.
|
||||
/// </summary>
|
||||
public sealed class RuntimeFirstEntryHostIntegrationTests
|
||||
{
|
||||
private const uint Cell = 0x01010001u;
|
||||
private const uint Guid = 0x70000301u;
|
||||
|
||||
[Fact]
|
||||
public void InitialCreate_ResidenceConductorReceipt_BindsWorldVisibilityExactlyOnce()
|
||||
{
|
||||
using var fixture = new HostFixture(playerGuid: 0u);
|
||||
int residencesBegan = 0;
|
||||
fixture.EntityObjects.BindInitialResidenceBeginNotification(
|
||||
_ => residencesBegan++);
|
||||
bool visibleAtMaterialize = true;
|
||||
fixture.Materializer.AfterMaterialize = record =>
|
||||
{
|
||||
// Clause 1: the sidecar exists but presentation stays suppressed
|
||||
// until the conductor's completion receipt binds it.
|
||||
visibleAtMaterialize = record.IsSpatiallyProjected
|
||||
|| record.IsSpatiallyVisible;
|
||||
};
|
||||
|
||||
fixture.Controller.OnCreate(Spawn(Guid, Cell));
|
||||
|
||||
Assert.Equal(1, residencesBegan);
|
||||
Assert.False(visibleAtMaterialize);
|
||||
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord record));
|
||||
// The residence was consumed by the conductor inside the Create
|
||||
// transaction's own pump.
|
||||
Assert.False(fixture.Runtime.HasActiveInitialCreateResidence(
|
||||
record.Canonical));
|
||||
Assert.Equal(Cell, record.Canonical.FullCellId);
|
||||
Assert.True(record.IsSpatiallyProjected);
|
||||
Assert.True(record.IsSpatiallyVisible);
|
||||
Assert.NotNull(record.PhysicsBody);
|
||||
Assert.Equal((record, true), Assert.Single(fixture.VisibilityEdges));
|
||||
AcDream.Plugin.Abstractions.WorldEntitySnapshot snapshot =
|
||||
Assert.Single(fixture.WorldState.Entities);
|
||||
Assert.Equal(record.WorldEntity!.Position, snapshot.Position);
|
||||
Assert.Equal(0, fixture.EntityObjects.Placements.PendingCount);
|
||||
Assert.Equal(0, fixture.FirstEntry.PendingCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeferredParentCreate_StaysInvisibleUntilParentReplay()
|
||||
{
|
||||
const uint parentGuid = 0x70000302u;
|
||||
const uint childGuid = 0x70000303u;
|
||||
using var fixture = new HostFixture(playerGuid: 0u);
|
||||
int residencesBegan = 0;
|
||||
fixture.EntityObjects.BindInitialResidenceBeginNotification(
|
||||
_ => residencesBegan++);
|
||||
|
||||
fixture.Controller.OnCreate(ParentedSpawn(childGuid, parentGuid));
|
||||
|
||||
// Retail queues the raw blob under the parent's GUID; nothing about
|
||||
// the child may escape — no canonical, no sidecar, no presentation.
|
||||
Assert.Equal(0, residencesBegan);
|
||||
Assert.False(fixture.Runtime.TryGetCanonical(childGuid, out _));
|
||||
Assert.False(fixture.Runtime.TryGetRecord(childGuid, out _));
|
||||
Assert.True(fixture.Runtime.ParentAttachments.ContainsDeferredCreate(
|
||||
childGuid,
|
||||
instanceSequence: 1));
|
||||
Assert.Empty(fixture.WorldState.Entities);
|
||||
Assert.Empty(fixture.VisibilityEdges);
|
||||
|
||||
fixture.Controller.OnCreate(Spawn(parentGuid, Cell));
|
||||
// The replayed child's residence was recorded mid-drain; the next
|
||||
// frame's pump (the per-frame retry phase) drives its conductor.
|
||||
fixture.FirstEntry.DriveAll();
|
||||
|
||||
Assert.Equal(2, residencesBegan);
|
||||
Assert.False(fixture.Runtime.ParentAttachments.ContainsDeferredCreate(
|
||||
childGuid,
|
||||
instanceSequence: 1));
|
||||
Assert.True(fixture.Runtime.TryGetCanonical(
|
||||
childGuid,
|
||||
out RuntimeEntityRecord child));
|
||||
Assert.False(fixture.Runtime.HasActiveInitialCreateResidence(child));
|
||||
Assert.Equal(0, fixture.FirstEntry.PendingCount);
|
||||
// The parented child is celless and presentation-suppressed until its
|
||||
// own attach/position flow — only the parent is world-visible.
|
||||
Assert.Equal(0u, child.FullCellId);
|
||||
Assert.False(fixture.Runtime.TryGetRecord(childGuid, out _));
|
||||
Assert.True(fixture.Runtime.TryGetRecord(
|
||||
parentGuid,
|
||||
out LiveEntityRecord parent));
|
||||
Assert.True(parent.IsSpatiallyVisible);
|
||||
Assert.Single(fixture.WorldState.Entities);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LocalLogin_PresentationAttachFailure_RetriesWithoutRuntimeRollback()
|
||||
{
|
||||
using var fixture = new HostFixture(playerGuid: Guid);
|
||||
// The camera/shadow-analog App attach failure: the first
|
||||
// world-visibility binding throws AFTER Runtime committed the
|
||||
// controller/body/placement.
|
||||
fixture.VisibilityFailuresRemaining = 1;
|
||||
|
||||
fixture.Controller.OnCreate(Spawn(Guid, Cell));
|
||||
|
||||
// Runtime is NOT rolled back by the App-side presentation failure:
|
||||
// the published movement controller, canonical body, and committed
|
||||
// cell all survive; only the completion receipt stays pending for
|
||||
// the per-frame retry.
|
||||
AcDream.Runtime.Gameplay.PlayerMovementController controller =
|
||||
Assert.IsType<AcDream.Runtime.Gameplay.PlayerMovementController>(
|
||||
fixture.Movement.Controller);
|
||||
Assert.True(controller.IsRuntimePublished);
|
||||
Assert.True(fixture.Runtime.TryGetRecord(Guid, out LiveEntityRecord record));
|
||||
Assert.Equal(Cell, record.Canonical.FullCellId);
|
||||
Assert.NotNull(record.PhysicsBody);
|
||||
Assert.False(fixture.Runtime.HasActiveInitialCreateResidence(
|
||||
record.Canonical));
|
||||
Assert.Equal(1, fixture.EntityObjects.Placements.PendingCount);
|
||||
Assert.Equal((record, true), Assert.Single(fixture.VisibilityEdges));
|
||||
|
||||
Assert.True(fixture.Subscription.RetryPending());
|
||||
|
||||
Assert.Same(controller, fixture.Movement.Controller);
|
||||
Assert.True(controller.IsRuntimePublished);
|
||||
Assert.Equal(0, fixture.EntityObjects.Placements.PendingCount);
|
||||
Assert.True(record.IsSpatiallyProjected);
|
||||
Assert.True(record.IsSpatiallyVisible);
|
||||
Assert.Equal(2, fixture.VisibilityEdges.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C3c-F5: through the REAL flipped host wiring (hydration ->
|
||||
/// residence -> conductor -> publication -> dormant activation), a
|
||||
/// login onto flat ground must complete with retail's
|
||||
/// first-gravity-frame contact (SmartBox::HandleCreateObject 0x00454C80
|
||||
/// -> init_player 0x00455010 -> CPhysicsObj::enter_world 0x00516170 +
|
||||
/// the first simulated frame's touch, compressed via the shared #270
|
||||
/// settle) — and the outbound motion snapshot must report grounded,
|
||||
/// the exact bit LocalPlayerOutboundController serializes and ACE's
|
||||
/// "You can't do that while in the air!" gate reads. Spawn feet at 5
|
||||
/// over a flat floor at 4.7 (0.3 m inside the settle reach), with the
|
||||
/// production human bottom-sphere origin so the authored placement
|
||||
/// stands clear of the floor.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void LocalLogin_FlatGround_ReportsGroundedOutboundContactBit()
|
||||
{
|
||||
using var fixture = new HostFixture(
|
||||
playerGuid: Guid,
|
||||
terrainHeight: 4.7f,
|
||||
moverSphereOriginZ: 0.475f);
|
||||
|
||||
fixture.Controller.OnCreate(Spawn(Guid, Cell));
|
||||
|
||||
AcDream.Runtime.Gameplay.PlayerMovementController controller =
|
||||
Assert.IsType<AcDream.Runtime.Gameplay.PlayerMovementController>(
|
||||
fixture.Movement.Controller);
|
||||
Assert.True(controller.IsRuntimePublished);
|
||||
Assert.True(fixture.Runtime.TryGetRecord(
|
||||
Guid,
|
||||
out LiveEntityRecord record));
|
||||
PhysicsBody body = Assert.IsType<PhysicsBody>(record.PhysicsBody);
|
||||
Assert.True(body.InWorld);
|
||||
Assert.True(body.InContact);
|
||||
Assert.True(body.OnWalkable);
|
||||
Assert.True(body.ContactPlaneValid);
|
||||
Assert.InRange(body.Position.Z, 4.65f, 4.76f);
|
||||
Assert.True(controller.CanSendPositionEvent);
|
||||
Assert.True(controller.CaptureMovementResult(
|
||||
mouseLookEvent: false).IsOnGround);
|
||||
Assert.Equal(0, fixture.FirstEntry.PendingCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C3c-F5 counterpart through the same real wiring: a login spawn with
|
||||
/// no floor within the settle's reach stays genuinely airborne — no
|
||||
/// forced grounding anywhere in the first-entry sequence.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void LocalLogin_AirborneSpawn_StaysGenuinelyAirborne()
|
||||
{
|
||||
using var fixture = new HostFixture(
|
||||
playerGuid: Guid,
|
||||
moverSphereOriginZ: 0.475f);
|
||||
|
||||
fixture.Controller.OnCreate(Spawn(Guid, Cell));
|
||||
|
||||
AcDream.Runtime.Gameplay.PlayerMovementController controller =
|
||||
Assert.IsType<AcDream.Runtime.Gameplay.PlayerMovementController>(
|
||||
fixture.Movement.Controller);
|
||||
Assert.True(controller.IsRuntimePublished);
|
||||
Assert.True(fixture.Runtime.TryGetRecord(
|
||||
Guid,
|
||||
out LiveEntityRecord record));
|
||||
PhysicsBody body = Assert.IsType<PhysicsBody>(record.PhysicsBody);
|
||||
Assert.True(body.InWorld);
|
||||
Assert.False(body.InContact);
|
||||
Assert.False(body.OnWalkable);
|
||||
Assert.False(controller.CanSendPositionEvent);
|
||||
Assert.False(controller.CaptureMovementResult(
|
||||
mouseLookEvent: false).IsOnGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GraphicalAndDirectHosts_CommitIdenticalFirstEntryRuntimeFacts()
|
||||
{
|
||||
// Graphical host: full flipped wiring.
|
||||
using var graphical = new HostFixture(playerGuid: 0u);
|
||||
graphical.Controller.OnCreate(Spawn(Guid, Cell));
|
||||
Assert.True(graphical.Runtime.TryGetCanonical(
|
||||
Guid,
|
||||
out RuntimeEntityRecord graphicalRecord));
|
||||
|
||||
// Direct (no-window) host: the same canonical machinery with no
|
||||
// presentation at all — registration, drive pump, ack-only
|
||||
// subscription (the headless host shape).
|
||||
LiveEntityRuntimeFixture.DrivenLiveEntityRuntime direct =
|
||||
LiveEntityRuntimeFixture.CreateDriven(
|
||||
new GpuWorldState(),
|
||||
new NoopResources());
|
||||
RuntimeEntityRecord directRecord = Assert.IsType<RuntimeEntityRecord>(
|
||||
direct.Lifetime.RegisterEntityWithInitialResidence(
|
||||
Spawn(Guid, Cell),
|
||||
isLocalPlayer: false).Canonical);
|
||||
Assert.True(direct.Lifetime.ApplyAcceptedSpawn(
|
||||
directRecord,
|
||||
directRecord.CreateIntegrationVersion,
|
||||
directRecord.Snapshot,
|
||||
replaceGeneration: false));
|
||||
direct.Pump();
|
||||
|
||||
Assert.Equal(
|
||||
FirstEntryFacts.Capture(graphicalRecord),
|
||||
FirstEntryFacts.Capture(directRecord));
|
||||
Assert.False(graphical.Runtime.HasActiveInitialCreateResidence(
|
||||
graphicalRecord));
|
||||
Assert.Equal(0, direct.FirstEntry.PendingCount);
|
||||
Assert.Equal(0, graphical.FirstEntry.PendingCount);
|
||||
}
|
||||
|
||||
private readonly record struct FirstEntryFacts(
|
||||
uint ServerGuid,
|
||||
ushort Incarnation,
|
||||
uint? LocalEntityId,
|
||||
uint FullCellId,
|
||||
uint CanonicalLandblockId,
|
||||
ulong PositionAuthorityVersion,
|
||||
ulong PlacementCommitVersion,
|
||||
ulong CreateIntegrationVersion,
|
||||
ushort SnapshotPositionSequence,
|
||||
bool HasBody,
|
||||
Vector3 BodyPosition,
|
||||
Quaternion BodyOrientation,
|
||||
PhysicsStateFlags BodyState,
|
||||
bool BodyInWorld)
|
||||
{
|
||||
internal static FirstEntryFacts Capture(RuntimeEntityRecord record) =>
|
||||
new(
|
||||
record.ServerGuid,
|
||||
record.Incarnation,
|
||||
record.LocalEntityId,
|
||||
record.FullCellId,
|
||||
record.CanonicalLandblockId,
|
||||
record.PositionAuthorityVersion,
|
||||
record.PlacementCommitVersion,
|
||||
record.CreateIntegrationVersion,
|
||||
record.Snapshot.PositionSequence,
|
||||
record.PhysicsBody is not null,
|
||||
record.PhysicsBody?.Position ?? default,
|
||||
record.PhysicsBody?.Orientation ?? default,
|
||||
record.PhysicsBody?.State ?? default,
|
||||
record.PhysicsBody?.InWorld ?? false);
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell)
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
|
||||
var timestamps = new PhysicsTimestamps(
|
||||
Position: 1,
|
||||
Movement: 1,
|
||||
State: 1,
|
||||
Vector: 1,
|
||||
Teleport: 0,
|
||||
ServerControlledMove: 1,
|
||||
ForcePosition: 0,
|
||||
ObjDesc: 1,
|
||||
Instance: 1);
|
||||
var physics = new PhysicsSpawnData(
|
||||
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||
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,
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
null,
|
||||
null,
|
||||
"first entry",
|
||||
(uint)ItemType.Creature,
|
||||
null,
|
||||
0x09000001u,
|
||||
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||
InstanceSequence: 1,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
PositionSequence: 1,
|
||||
Physics: physics);
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn ParentedSpawn(
|
||||
uint guid,
|
||||
uint parentGuid)
|
||||
{
|
||||
var timestamps = new PhysicsTimestamps(
|
||||
Position: 1,
|
||||
Movement: 1,
|
||||
State: 1,
|
||||
Vector: 1,
|
||||
Teleport: 0,
|
||||
ServerControlledMove: 1,
|
||||
ForcePosition: 0,
|
||||
ObjDesc: 1,
|
||||
Instance: 1);
|
||||
var physics = new PhysicsSpawnData(
|
||||
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||
Position: null,
|
||||
Movement: null,
|
||||
AnimationFrame: 1u,
|
||||
SetupTableId: 0x02000001u,
|
||||
MotionTableId: 0x09000001u,
|
||||
SoundTableId: null,
|
||||
PhysicsScriptTableId: null,
|
||||
Parent: new PhysicsAttachment(parentGuid, LocationId: 1u),
|
||||
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: null,
|
||||
SetupTableId: 0x02000001u,
|
||||
AnimPartChanges: [],
|
||||
TextureChanges: [],
|
||||
SubPalettes: [],
|
||||
BasePaletteId: null,
|
||||
ObjScale: null,
|
||||
Name: "deferred child",
|
||||
ItemType: (uint)ItemType.Creature,
|
||||
MotionState: null,
|
||||
MotionTableId: 0x09000001u,
|
||||
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
|
||||
InstanceSequence: 1,
|
||||
MovementSequence: 1,
|
||||
ServerControlSequence: 1,
|
||||
PositionSequence: 1,
|
||||
ParentGuid: parentGuid,
|
||||
ParentLocation: 1u,
|
||||
PlacementId: 1u,
|
||||
Physics: physics);
|
||||
}
|
||||
|
||||
private sealed class HostFixture : IDisposable
|
||||
{
|
||||
internal readonly RuntimeEntityObjectLifetime EntityObjects = new();
|
||||
internal readonly LiveEntityRuntime Runtime;
|
||||
internal readonly LiveEntityHydrationController Controller;
|
||||
internal readonly HostMaterializer Materializer;
|
||||
internal readonly AcDream.Runtime.Session.RuntimeFirstEntryDriveController
|
||||
FirstEntry;
|
||||
internal readonly AcDream.Runtime.Physics
|
||||
.RuntimePlacementProjectionSubscription Subscription;
|
||||
internal readonly AcDream.Runtime.Gameplay.RuntimeLocalPlayerMovementState
|
||||
Movement;
|
||||
internal readonly WorldGameState WorldState = new();
|
||||
internal readonly List<(LiveEntityRecord Record, bool Visible)>
|
||||
VisibilityEdges = [];
|
||||
internal int VisibilityFailuresRemaining;
|
||||
|
||||
internal HostFixture(
|
||||
uint playerGuid,
|
||||
float terrainHeight = 0f,
|
||||
float moverSphereOriginZ = 0f)
|
||||
{
|
||||
EntityObjects.BindEventContext(
|
||||
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
|
||||
static () => 1UL);
|
||||
EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
||||
Cell & 0xFFFF0000u, 1UL);
|
||||
EntityObjects.Physics.Engine.AddLandblock(
|
||||
Cell & 0xFFFF0000u,
|
||||
new TerrainSurface(
|
||||
new byte[81],
|
||||
Enumerable.Repeat(terrainHeight, 256).ToArray()),
|
||||
Array.Empty<CellSurface>(),
|
||||
Array.Empty<PortalPlane>(),
|
||||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f);
|
||||
EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
||||
Cell & 0xFFFF0000u, 1UL, ready: true);
|
||||
Movement = new AcDream.Runtime.Gameplay
|
||||
.RuntimeLocalPlayerMovementState();
|
||||
var runtimeIdentity = new AcDream.Runtime.Gameplay
|
||||
.RuntimeLocalPlayerIdentityState();
|
||||
var publication = new AcDream.Runtime.Gameplay
|
||||
.RuntimeLocalPlayerPhysicsPublicationState(
|
||||
EntityObjects.Entities,
|
||||
EntityObjects.Physics,
|
||||
Movement,
|
||||
runtimeIdentity);
|
||||
Movement.AttachPhysicsPublication(publication);
|
||||
EntityObjects.LocalPlayerFirstEntry.BindPublication(publication);
|
||||
runtimeIdentity.ServerGuid = playerGuid;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(new LoadedLandblock(
|
||||
(Cell & 0xFFFF0000u) | 0xFFFFu,
|
||||
new LandBlock(),
|
||||
Array.Empty<WorldEntity>()));
|
||||
Runtime = new LiveEntityRuntime(
|
||||
spatial,
|
||||
new NoopResources(),
|
||||
EntityObjects);
|
||||
FirstEntry = new AcDream.Runtime.Session
|
||||
.RuntimeFirstEntryDriveController(
|
||||
EntityObjects,
|
||||
new AcDream.Runtime.GameRuntimeClock(),
|
||||
new SphereCollisionSource(moverSphereOriginZ),
|
||||
static () => AcDream.Runtime.Gameplay
|
||||
.PlayerMovementConstructionOptions.Fallback,
|
||||
static _ => new AcDream.Runtime.Gameplay
|
||||
.RuntimeLocalPlayerPhysicsActivationPreparation(
|
||||
0.48f,
|
||||
1.835f,
|
||||
AcDream.Runtime.Gameplay
|
||||
.RuntimeLocalPlayerShadowDisposition
|
||||
.ProvenShapeless));
|
||||
var sink = new RuntimePlacementPresentationSink(
|
||||
Runtime,
|
||||
new RuntimeWorldTransitState(),
|
||||
WorldState,
|
||||
new WorldEvents(),
|
||||
new EntityEffectPoseRegistry(),
|
||||
new LocalPlayerShadowState(),
|
||||
() => playerGuid,
|
||||
_ => { },
|
||||
[
|
||||
(record, visible) =>
|
||||
{
|
||||
VisibilityEdges.Add((record, visible));
|
||||
if (VisibilityFailuresRemaining > 0)
|
||||
{
|
||||
VisibilityFailuresRemaining--;
|
||||
throw new InvalidOperationException(
|
||||
"fixture presentation attach failure");
|
||||
}
|
||||
},
|
||||
]);
|
||||
Subscription = new AcDream.Runtime.Physics
|
||||
.RuntimePlacementProjectionSubscription(
|
||||
EntityObjects.Placements,
|
||||
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
|
||||
sink);
|
||||
Materializer = new HostMaterializer(Runtime);
|
||||
var identity = new LocalPlayerIdentityState
|
||||
{
|
||||
ServerGuid = playerGuid,
|
||||
};
|
||||
var dormant = new DormantLiveEntityStore();
|
||||
var teardown = new NoopTeardown();
|
||||
var deletion = new LiveEntityDeletionController(
|
||||
Runtime,
|
||||
EntityObjects,
|
||||
teardown,
|
||||
identity,
|
||||
dormant);
|
||||
Controller = new LiveEntityHydrationController(
|
||||
Runtime,
|
||||
EntityObjects,
|
||||
new object(),
|
||||
Materializer,
|
||||
new NoopRelationships(),
|
||||
new AcceptingReady(),
|
||||
new KnownOrigin(),
|
||||
new NoopNetworkSink(),
|
||||
new NoopTimestamps(),
|
||||
identity,
|
||||
deletion,
|
||||
dormant,
|
||||
firstEntry: FirstEntry);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
Runtime.Clear();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Failure-path tests assert their own exceptions.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors the production materializer
|
||||
/// (DatLiveEntityProjectionMaterializer.MaterializeProjection): route-1
|
||||
/// world creates materialize residence-managed and self-project only when
|
||||
/// the committed cell already exists with no active residence.
|
||||
/// </summary>
|
||||
private sealed class HostMaterializer(LiveEntityRuntime runtime)
|
||||
: ILiveEntityProjectionMaterializer
|
||||
{
|
||||
internal Action<LiveEntityRecord>? AfterMaterialize { get; set; }
|
||||
|
||||
public bool TryMaterialize(
|
||||
RuntimeEntityRecord expectedCanonical,
|
||||
WorldSession.EntitySpawn canonicalSpawn,
|
||||
LiveProjectionPurpose purpose,
|
||||
ulong expectedCreateIntegrationVersion,
|
||||
AcDream.App.Rendering.LiveEntityAppearanceUpdateState? appearanceUpdate = null)
|
||||
{
|
||||
if (canonicalSpawn.Position is not { } position
|
||||
|| canonicalSpawn.SetupTableId is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
WorldEntity? entity = runtime.MaterializeLiveEntity(
|
||||
expectedCanonical,
|
||||
position.LandblockId,
|
||||
id => new WorldEntity
|
||||
{
|
||||
Id = id,
|
||||
ServerGuid = canonicalSpawn.Guid,
|
||||
SourceGfxObjOrSetupId = canonicalSpawn.SetupTableId.Value,
|
||||
Position = new Vector3(
|
||||
position.PositionX,
|
||||
position.PositionY,
|
||||
position.PositionZ),
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = [],
|
||||
ParentCellId = position.LandblockId,
|
||||
},
|
||||
LiveEntityProjectionKind.World,
|
||||
initializeProjection: null,
|
||||
out LiveEntityRecord? record,
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement);
|
||||
if (entity is null || record is null)
|
||||
return false;
|
||||
if (runtime.IsCurrentCreateIntegration(
|
||||
expectedCanonical,
|
||||
expectedCreateIntegrationVersion)
|
||||
&& expectedCanonical.FullCellId != 0u
|
||||
&& !runtime.HasActiveInitialCreateResidence(expectedCanonical)
|
||||
&& !runtime.RebucketLiveEntity(
|
||||
canonicalSpawn.Guid,
|
||||
expectedCanonical.FullCellId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
AfterMaterialize?.Invoke(record);
|
||||
return runtime.IsCurrentRecord(record);
|
||||
}
|
||||
|
||||
public void ResetSessionState()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NoopResources : ILiveEntityResourceLifecycle
|
||||
{
|
||||
public void Register(WorldEntity entity)
|
||||
{
|
||||
}
|
||||
|
||||
public void Unregister(WorldEntity entity)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NoopTeardown : ILiveEntityTeardownCoordinator
|
||||
{
|
||||
public void TearDown(LiveEntityRecord record)
|
||||
{
|
||||
}
|
||||
|
||||
public void ForgetUnknownOwner(uint serverGuid)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NoopRelationships : ILiveEntityRelationshipProjection
|
||||
{
|
||||
public void OnSpawn(WorldSession.EntitySpawn spawn)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnParent(ParentEvent.Parsed update)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnCreateParentAccepted(CreateParentUpdate update)
|
||||
{
|
||||
}
|
||||
|
||||
public ChildUnparentDisposition OnChildBecameUnparented(uint childGuid) =>
|
||||
ChildUnparentDisposition.Completed;
|
||||
|
||||
public bool TryApplyAttachedAppearance(
|
||||
LiveEntityRecord record,
|
||||
ulong objDescAuthorityVersion) => false;
|
||||
}
|
||||
|
||||
private sealed class AcceptingReady : ILiveEntityReadyPublisher
|
||||
{
|
||||
public bool Publish(LiveEntityReadyCandidate candidate) => true;
|
||||
}
|
||||
|
||||
private sealed class KnownOrigin : ILiveEntityWorldOriginCoordinator
|
||||
{
|
||||
public bool IsKnown => true;
|
||||
|
||||
public LiveEntityOriginInitialization TryInitialize(
|
||||
WorldSession.EntitySpawn spawn) => new(true, []);
|
||||
}
|
||||
|
||||
private sealed class NoopNetworkSink : ILiveEntityNetworkUpdateSink
|
||||
{
|
||||
public void ApplySameGeneration(SameGenerationCreateObjectEvents events)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class NoopTimestamps : IAcceptedLocalPhysicsTimestampPublisher
|
||||
{
|
||||
public void Publish(uint serverGuid, AcceptedPhysicsTimestamps timestamps)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SphereCollisionSource(float sphereOriginZ = 0f)
|
||||
: AcDream.Content.IPreparedCollisionSource
|
||||
{
|
||||
public AcDream.Content.PreparedAssetPresence ProbeCollision(
|
||||
AcDream.Content.Pak.PakAssetType type,
|
||||
uint sourceFileId) =>
|
||||
AcDream.Content.PreparedAssetPresence.Available;
|
||||
|
||||
public AcDream.Content.PreparedCollisionReadResult<
|
||||
FlatSetupCollision> ReadSetupCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
AcDream.Content.PreparedCollisionReadResult<FlatSetupCollision>
|
||||
.Loaded(new FlatSetupCollision(
|
||||
System.Collections.Immutable.ImmutableArray<
|
||||
FlatCollisionCylinder>.Empty,
|
||||
[new FlatCollisionSphere(
|
||||
new Vector3(0f, 0f, sphereOriginZ),
|
||||
0.48f)],
|
||||
height: 0f,
|
||||
radius: 0f,
|
||||
stepUpHeight: 0.4f,
|
||||
stepDownHeight: 0.4f));
|
||||
|
||||
public AcDream.Content.PreparedCollisionReadResult<
|
||||
FlatGfxObjCollisionAsset> ReadGfxObjCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public AcDream.Content.PreparedCollisionReadResult<
|
||||
FlatCellStructureCollisionAsset> ReadCellStructureCollision(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public AcDream.Content.PreparedCollisionReadResult<
|
||||
FlatEnvCellTopology> ReadEnvCellTopology(
|
||||
uint sourceFileId,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public AcDream.Content.PreparedCollisionSourceStats CollisionStats =>
|
||||
default;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -757,6 +757,16 @@ public sealed class RuntimePlacementPresentationSinkTests
|
|||
internal LiveEntityRecord Materialize(WorldSession.EntitySpawn spawn)
|
||||
{
|
||||
LiveEntityRecord record = Runtime.RegisterAndMaterializeProjection(spawn);
|
||||
// C3c fixture normalization: RegisterAndMaterializeProjection's
|
||||
// legacy-immediate rebucket commits the wire cell out-of-band of
|
||||
// the fresh initial-create residence, leaving that residence
|
||||
// stale. Converge it deterministically HERE (the query performs
|
||||
// the lazy retirement, releasing the residence's never-driven
|
||||
// initial SetPosition operation) so ownership snapshots captured
|
||||
// by tests reflect the settled post-registration state instead of
|
||||
// shifting inside the sink's own first residence-gate query.
|
||||
Assert.False(Runtime.HasActiveInitialCreateResidence(
|
||||
record.Canonical));
|
||||
Assert.True(record.ResourcesRegistered);
|
||||
WorldEntity entity = record.WorldEntity!;
|
||||
var snapshot = new AcDream.Plugin.Abstractions.WorldEntitySnapshot(
|
||||
|
|
|
|||
|
|
@ -740,16 +740,22 @@ public sealed class UpdateFrameOrchestratorTests
|
|||
"AcDream.App",
|
||||
"Input",
|
||||
"PlayerModeController.cs"));
|
||||
// C3c (clause 4 — sealed-setter lifecycle routing): the movement
|
||||
// controller, physics body, host, and committed placement are
|
||||
// Runtime-owned, published by the first-entry conductor's
|
||||
// publication transaction. Player-mode entry attaches presentation
|
||||
// only: it gates on the Runtime-published controller, then wires
|
||||
// camera -> shadow -> host slot -> mode flag. The old pinned
|
||||
// markers (PreparePositionForCommit / InstallOrRebind /
|
||||
// CommitPreparedPosition / `_controllerSlot.Controller =`) were
|
||||
// exactly the App-side controller construction+commit this flip
|
||||
// deleted.
|
||||
AssertAppearsInOrder(
|
||||
playerModeSource,
|
||||
"controller.PreparePositionForCommit(",
|
||||
"controller.IsRuntimePublished",
|
||||
"_camera.EnterChaseMode(legacyCamera, retailCamera);",
|
||||
"EntityPhysicsHostComposition.SelectStableHostWithoutRebind(",
|
||||
"_shadow.SyncPose(",
|
||||
"EntityPhysicsHostComposition.InstallOrRebind(",
|
||||
"playerEntity.SetPosition(initial.Position);",
|
||||
"controller.CommitPreparedPosition();",
|
||||
"_controllerSlot.Controller = controller;",
|
||||
"_hostSlot.Host = playerHost;",
|
||||
"_mode.IsPlayerMode = true;");
|
||||
Assert.Contains("_shadow.Restore(playerEntity, priorShadow);", playerModeSource,
|
||||
StringComparison.Ordinal);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue