670f307cmade remote first-entry placement resolve its landblock-local CreateObject origin through Runtime's world frame (RuntimeSetPositionState.PrepareMover:1526-1544) and return RetrySetupUnavailable until that frame exists. Only the accepted local-player Create publishes it (RuntimeEntityObjectLifetime.RegisterEntityCore:558-570 -> RuntimePhysicsState.ObserveLocalWorldFrame). Fixtures that drive remote conductors in a world with no local player - a state production never occupies, since the player's own Create always precedes broadcast Creates - therefore parked forever on RetrySetupUnavailable. Their initial-create residences never retired, which cascaded into rejected appearance updates, missing canonical bodies, unconverged ownership ledgers, and a GameRuntime teardown that could not complete stage 10. The measured blast radius was far larger than the handoff recorded. It claimed "six selected fixture failures"; a baseline run found 43. The App suite was fully green at01f4791eand670f307cbroke 28 tests at once; the Runtime suite lost 13, twelve of them in RuntimeRemoteFirstEntryStateTests - the exact conductor that commit gated. Both commits were verified on focused runs only. The production gate is correct, so nothing here weakens it. It matches App's own coordinate owner: LiveWorldOriginState is initialized once from the local player's spawn (LiveEntityHydrationPorts.cs:226) and rebased only by StreamingOriginRecenterCoordinator.Advance at a teleport boundary - exactly ObserveLocalWorldFrame's semantics. Every fixture is repaired by supplying the missing precondition beside the resident landblock it already models, and not one expected value or assertion was changed. The mechanism shipped with zero tests. RuntimeWorldFrameTests now pins its contract: the local player publishes the frame, remotes never do, neighbouring landblocks convert at 192 m per step, ordinary movement across a landblock boundary must NOT rebase it, an accepted teleport must, and a zero cell id neither publishes nor resolves. That "no rebase on ordinary movement" rule is load-bearing - if it and LiveWorldOriginState ever disagree, remote objects are placed a multiple of 192 m from where the world is streamed. Runtime 1,009/1,009; App 4,048 passed / 3 skipped. Refs #281. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
275 lines
11 KiB
C#
275 lines
11 KiB
C#
using AcDream.App.Streaming;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Runtime.Entities;
|
|
|
|
namespace AcDream.App.Tests;
|
|
|
|
/// <summary>
|
|
/// Explicit isolated lifetime-group seam for App projection tests. Production
|
|
/// composition supplies its session-owned group from GameWindow; focused tests
|
|
/// use a fresh group per runtime so identity never leaks between fixtures.
|
|
/// </summary>
|
|
internal static class LiveEntityRuntimeFixture
|
|
{
|
|
/// <summary>
|
|
/// C3c: initial-Create registration now begins the canonical residence
|
|
/// lease, whose admission requires a live session generation. Focused
|
|
/// App fixtures bind a fixed non-zero token exactly like the Runtime
|
|
/// conductor fixtures do.
|
|
/// </summary>
|
|
private static RuntimeEntityObjectLifetime WithGeneration(
|
|
RuntimeEntityObjectLifetime lifetime)
|
|
{
|
|
lifetime.BindEventContext(
|
|
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
|
|
static () => 1UL);
|
|
return lifetime;
|
|
}
|
|
|
|
public static LiveEntityRuntime Create(
|
|
GpuWorldState spatial,
|
|
ILiveEntityResourceLifecycle resources,
|
|
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
|
|
{
|
|
var lifetime = WithGeneration(
|
|
new RuntimeEntityObjectLifetime(firstLocalEntityId));
|
|
return new LiveEntityRuntime(spatial, resources, lifetime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// C3c: a runtime whose initial-create residences can actually be driven
|
|
/// to completion — collision generation committed for
|
|
/// <paramref name="landblockId"/>, the production
|
|
/// <see cref="AcDream.Runtime.Session.RuntimeFirstEntryDriveController"/>
|
|
/// constructed against the lifetime, and an acknowledge-only placement
|
|
/// subscription mirroring the host rules (Discard/ExecutorCompleted
|
|
/// acknowledged; Place/Withdraw left at the head for the conductors).
|
|
/// Tests exercising post-residence legacy update paths register, then
|
|
/// call <see cref="DrivenLiveEntityRuntime.Pump"/> to complete the
|
|
/// conductors exactly like the composed host's Create-transaction pump.
|
|
/// </summary>
|
|
internal sealed class DrivenLiveEntityRuntime
|
|
{
|
|
internal required LiveEntityRuntime Runtime { get; init; }
|
|
internal required RuntimeEntityObjectLifetime Lifetime { get; init; }
|
|
internal required AcDream.Runtime.Session.RuntimeFirstEntryDriveController
|
|
FirstEntry { get; init; }
|
|
internal required AcDream.Runtime.Physics
|
|
.RuntimePlacementProjectionSubscription Subscription { get; init; }
|
|
|
|
internal void Pump() => FirstEntry.DriveAll();
|
|
}
|
|
|
|
public static DrivenLiveEntityRuntime CreateDriven(
|
|
GpuWorldState spatial,
|
|
ILiveEntityResourceLifecycle resources,
|
|
uint landblockId = 0x01010000u)
|
|
{
|
|
var lifetime = WithGeneration(new RuntimeEntityObjectLifetime());
|
|
lifetime.Physics.SetPosition.BeginCollisionGeneration(
|
|
landblockId & 0xFFFF0000u, 1UL);
|
|
lifetime.Physics.Engine.AddLandblock(
|
|
landblockId & 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);
|
|
lifetime.Physics.SetPosition.CommitCollisionGeneration(
|
|
landblockId & 0xFFFF0000u, 1UL, ready: true);
|
|
|
|
// 670f307c: Create frames are landblock-local, so a first-entry
|
|
// conductor resolves them through Runtime's world frame
|
|
// (RuntimeSetPositionState.PrepareMover:1526-1544) and parks on
|
|
// RetrySetupUnavailable until that frame exists. Only the accepted
|
|
// local-player Create publishes it
|
|
// (RuntimeEntityObjectLifetime.RegisterEntityCore:558-570 ->
|
|
// RuntimePhysicsState.ObserveLocalWorldFrame), and production always
|
|
// registers the player before driving any broadcast Create. Model
|
|
// that ambient world fact beside the resident landblock above rather
|
|
// than registering a second entity, which would consume a local
|
|
// entity id and perturb every exact-ledger assertion built on this
|
|
// fixture. RuntimeWorldFrameTests covers the real ordering.
|
|
lifetime.Physics.ObserveLocalWorldFrame(
|
|
(landblockId & 0xFFFF0000u) | 0x0001u,
|
|
teleportAdvanced: false);
|
|
var runtime = new LiveEntityRuntime(spatial, resources, lifetime);
|
|
var movement = new AcDream.Runtime.Gameplay
|
|
.RuntimeLocalPlayerMovementState();
|
|
var identity = new AcDream.Runtime.Gameplay
|
|
.RuntimeLocalPlayerIdentityState();
|
|
var publication = new AcDream.Runtime.Gameplay
|
|
.RuntimeLocalPlayerPhysicsPublicationState(
|
|
lifetime.Entities,
|
|
lifetime.Physics,
|
|
movement,
|
|
identity);
|
|
movement.AttachPhysicsPublication(publication);
|
|
lifetime.LocalPlayerFirstEntry.BindPublication(publication);
|
|
var firstEntry = new AcDream.Runtime.Session
|
|
.RuntimeFirstEntryDriveController(
|
|
lifetime,
|
|
new AcDream.Runtime.GameRuntimeClock(),
|
|
new SphereCollisionSource(),
|
|
static () => AcDream.Runtime.Gameplay
|
|
.PlayerMovementConstructionOptions.Fallback,
|
|
static _ => new AcDream.Runtime.Gameplay
|
|
.RuntimeLocalPlayerPhysicsActivationPreparation(
|
|
0.48f,
|
|
1.835f,
|
|
AcDream.Runtime.Gameplay
|
|
.RuntimeLocalPlayerShadowDisposition
|
|
.ProvenShapeless));
|
|
var subscription = new AcDream.Runtime.Physics
|
|
.RuntimePlacementProjectionSubscription(
|
|
lifetime.Placements,
|
|
static () => new AcDream.Runtime.RuntimeGenerationToken(1UL),
|
|
new AckOnlyPlacementSink(runtime));
|
|
|
|
return new DrivenLiveEntityRuntime
|
|
{
|
|
Runtime = runtime,
|
|
Lifetime = lifetime,
|
|
FirstEntry = firstEntry,
|
|
Subscription = subscription,
|
|
};
|
|
}
|
|
|
|
private sealed class AckOnlyPlacementSink(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);
|
|
}
|
|
}
|
|
|
|
private sealed class SphereCollisionSource
|
|
: 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 static LiveEntityRuntime Create(
|
|
GpuWorldState spatial,
|
|
ILiveEntityResourceLifecycle resources,
|
|
PhysicsEngine physicsEngine,
|
|
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
|
|
{
|
|
var lifetime = WithGeneration(new RuntimeEntityObjectLifetime(
|
|
physicsEngine,
|
|
firstLocalEntityId));
|
|
return new LiveEntityRuntime(spatial, resources, lifetime);
|
|
}
|
|
|
|
public static LiveEntityRuntime Create(
|
|
GpuWorldState spatial,
|
|
ILiveEntityResourceLifecycle resources,
|
|
Action<LiveEntityRecord> tearDownRuntimeComponents,
|
|
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
|
|
{
|
|
var lifetime = WithGeneration(
|
|
new RuntimeEntityObjectLifetime(firstLocalEntityId));
|
|
return new LiveEntityRuntime(
|
|
spatial,
|
|
resources,
|
|
tearDownRuntimeComponents,
|
|
lifetime);
|
|
}
|
|
|
|
public static LiveEntityRuntime Create(
|
|
GpuWorldState spatial,
|
|
ILiveEntityResourceLifecycle resources,
|
|
ILiveEntityRuntimeComponentLifecycle runtimeComponentLifecycle,
|
|
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
|
|
{
|
|
var lifetime = WithGeneration(
|
|
new RuntimeEntityObjectLifetime(firstLocalEntityId));
|
|
return new LiveEntityRuntime(
|
|
spatial,
|
|
resources,
|
|
runtimeComponentLifecycle,
|
|
lifetime);
|
|
}
|
|
|
|
public static LiveEntityRuntime Create(
|
|
GpuWorldState spatial,
|
|
ILiveEntityResourceLifecycle resources,
|
|
ILiveEntityRuntimeComponentLifecycle runtimeComponentLifecycle,
|
|
PhysicsEngine physicsEngine,
|
|
uint firstLocalEntityId = RuntimeEntityDirectory.FirstLocalEntityId)
|
|
{
|
|
var lifetime = WithGeneration(new RuntimeEntityObjectLifetime(
|
|
physicsEngine,
|
|
firstLocalEntityId));
|
|
return new LiveEntityRuntime(
|
|
spatial,
|
|
resources,
|
|
runtimeComponentLifecycle,
|
|
lifetime);
|
|
}
|
|
}
|