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
|
|
@ -238,11 +238,25 @@ public sealed class HeadlessSessionHostTests
|
|||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||
operations);
|
||||
GameRuntime runtime = host.Runtime;
|
||||
// C3c: conductor-driven flow — a live generation admits the initial
|
||||
// residence, the flat landblock's collision generation commits, and
|
||||
// the world projection's pump drives the local first-entry conductor
|
||||
// to completion (the deleted SynchronizeLocalPlayer hand-copy's
|
||||
// replacement).
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
host.Start().Status);
|
||||
const uint player = 0x50000002u;
|
||||
runtime.PlayerIdentity.ServerGuid = player;
|
||||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
||||
0xA9B40000u, 1UL);
|
||||
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
||||
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
||||
0xA9B40000u, 1UL, ready: true);
|
||||
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
|
||||
CreateFirstEntryDrive(runtime);
|
||||
RuntimeEntityRecord record = runtime.EntityObjects
|
||||
.RegisterEntity(Spawn(player))
|
||||
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
|
||||
.Canonical!;
|
||||
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||
record,
|
||||
|
|
@ -252,7 +266,8 @@ public sealed class HeadlessSessionHostTests
|
|||
var collision = new FixtureCollisionNeighborhood();
|
||||
var projection = new HeadlessSessionWorldProjection(
|
||||
runtime,
|
||||
collision);
|
||||
collision,
|
||||
firstEntry);
|
||||
|
||||
projection.ProjectSpawn(record, isLocalPlayer: true);
|
||||
PlayerMovementController controller =
|
||||
|
|
@ -310,11 +325,25 @@ public sealed class HeadlessSessionHostTests
|
|||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||
operations);
|
||||
GameRuntime runtime = host.Runtime;
|
||||
// C3c: conductor-driven flow — a live generation admits the initial
|
||||
// residence, the flat landblock's collision generation commits, and
|
||||
// the world projection's pump drives the local first-entry conductor
|
||||
// to completion (the deleted SynchronizeLocalPlayer hand-copy's
|
||||
// replacement).
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
host.Start().Status);
|
||||
const uint player = 0x50000003u;
|
||||
runtime.PlayerIdentity.ServerGuid = player;
|
||||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
||||
0xA9B40000u, 1UL);
|
||||
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
||||
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
||||
0xA9B40000u, 1UL, ready: true);
|
||||
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
|
||||
CreateFirstEntryDrive(runtime);
|
||||
RuntimeEntityRecord record = runtime.EntityObjects
|
||||
.RegisterEntity(Spawn(player))
|
||||
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
|
||||
.Canonical!;
|
||||
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||
record,
|
||||
|
|
@ -324,7 +353,8 @@ public sealed class HeadlessSessionHostTests
|
|||
var collision = new FixtureCollisionNeighborhood();
|
||||
var projection = new HeadlessSessionWorldProjection(
|
||||
runtime,
|
||||
collision);
|
||||
collision,
|
||||
firstEntry);
|
||||
projection.ProjectSpawn(record, isLocalPlayer: true);
|
||||
PlayerMovementController controller =
|
||||
Assert.IsType<PlayerMovementController>(
|
||||
|
|
@ -376,6 +406,104 @@ public sealed class HeadlessSessionHostTests
|
|||
Assert.Equal(2, collision.CenterCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// C3c-R1 review F7: a remote Create whose landblock lies outside the
|
||||
/// bounded collision neighborhood's service window can never see its
|
||||
/// deferred placement's collision-generation wake — left alone it would
|
||||
/// pin its residence (and the drive's pending entry) for the whole
|
||||
/// session. The host converts it to the celless completion route before
|
||||
/// the pump: the residence completes with FullCell 0, the accepted wire
|
||||
/// frame stays on the canonical snapshot (the exact pre-flip
|
||||
/// accepted-frame behavior for far remotes), and every ledger converges.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void FarRemoteCreateCompletesCelllessWithoutPinningItsResidence()
|
||||
{
|
||||
var operations = new FixtureSessionOperations();
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||
operations);
|
||||
GameRuntime runtime = host.Runtime;
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
host.Start().Status);
|
||||
const uint player = 0x5000000Bu;
|
||||
runtime.PlayerIdentity.ServerGuid = player;
|
||||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
||||
0xA9B40000u, 1UL);
|
||||
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
||||
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
||||
0xA9B40000u, 1UL, ready: true);
|
||||
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
|
||||
CreateFirstEntryDrive(runtime);
|
||||
var collision = new FixtureCollisionNeighborhood();
|
||||
var projection = new HeadlessSessionWorldProjection(
|
||||
runtime,
|
||||
collision,
|
||||
firstEntry);
|
||||
RuntimeEntityRecord playerRecord = runtime.EntityObjects
|
||||
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
|
||||
.Canonical!;
|
||||
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||
playerRecord,
|
||||
playerRecord.CreateIntegrationVersion,
|
||||
playerRecord.Snapshot,
|
||||
replaceGeneration: false));
|
||||
projection.ProjectSpawn(playerRecord, isLocalPlayer: true);
|
||||
|
||||
const uint farRemote = 0x70000010u;
|
||||
const uint farCell = 0x00010001u;
|
||||
Assert.False(collision.IsWithinServiceWindow(farCell));
|
||||
RuntimeEntityRecord remote = runtime.EntityObjects
|
||||
.RegisterEntityWithInitialResidence(
|
||||
Spawn(farRemote, farCell),
|
||||
isLocalPlayer: false)
|
||||
.Canonical!;
|
||||
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||
remote,
|
||||
remote.CreateIntegrationVersion,
|
||||
remote.Snapshot,
|
||||
replaceGeneration: false));
|
||||
|
||||
projection.ProjectSpawn(remote, isLocalPlayer: false);
|
||||
|
||||
// Celless completion: no pinned residence, FullCell stays 0, the
|
||||
// accepted wire frame survives on the canonical snapshot.
|
||||
Assert.False(runtime.EntityObjects.TryGetInitialCreateResidence(
|
||||
remote,
|
||||
out _));
|
||||
Assert.Equal(0u, remote.FullCellId);
|
||||
Assert.Equal(
|
||||
farCell,
|
||||
remote.Snapshot.Position!.Value.LandblockId);
|
||||
RuntimeEntityObjectOwnershipSnapshot ownership =
|
||||
runtime.EntityObjects.CaptureOwnership();
|
||||
Assert.Equal(0, ownership.InitialCreateResidenceLeaseCount);
|
||||
Assert.Equal(0, ownership.FirstEntryDrivePendingCount);
|
||||
Assert.Equal(0, firstEntry.PendingCount);
|
||||
|
||||
// Draining the placement FIFO the way the host subscription would
|
||||
// converges the completion-receipt ledger too.
|
||||
while (runtime.EntityObjects.Physics.SetPosition.TryPeekProjection(
|
||||
out RuntimePlacementProjectionSnapshot head))
|
||||
{
|
||||
if (!runtime.EntityObjects.Physics.SetPosition
|
||||
.AcknowledgeProjection(head.Token))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
Assert.Equal(
|
||||
0,
|
||||
runtime.EntityObjects.CaptureOwnership()
|
||||
.PendingCompletionReceiptCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PlacementReceiptValidationDoesNotRegainMovementOrPhysicsAuthority()
|
||||
{
|
||||
|
|
@ -389,11 +517,25 @@ public sealed class HeadlessSessionHostTests
|
|||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||
operations);
|
||||
GameRuntime runtime = host.Runtime;
|
||||
// C3c: conductor-driven flow — a live generation admits the initial
|
||||
// residence, the flat landblock's collision generation commits, and
|
||||
// the world projection's pump drives the local first-entry conductor
|
||||
// to completion (the deleted SynchronizeLocalPlayer hand-copy's
|
||||
// replacement).
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
host.Start().Status);
|
||||
const uint player = 0x50000004u;
|
||||
runtime.PlayerIdentity.ServerGuid = player;
|
||||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
||||
0xA9B40000u, 1UL);
|
||||
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
||||
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
||||
0xA9B40000u, 1UL, ready: true);
|
||||
AcDream.Runtime.Session.RuntimeFirstEntryDriveController firstEntry =
|
||||
CreateFirstEntryDrive(runtime);
|
||||
RuntimeEntityRecord record = runtime.EntityObjects
|
||||
.RegisterEntity(Spawn(player))
|
||||
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
|
||||
.Canonical!;
|
||||
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||
record,
|
||||
|
|
@ -402,7 +544,8 @@ public sealed class HeadlessSessionHostTests
|
|||
replaceGeneration: false));
|
||||
var directProjection = new HeadlessSessionWorldProjection(
|
||||
runtime,
|
||||
new FixtureCollisionNeighborhood());
|
||||
new FixtureCollisionNeighborhood(),
|
||||
firstEntry);
|
||||
directProjection.ProjectSpawn(record, isLocalPlayer: true);
|
||||
PlayerMovementController controller = Assert.IsType<
|
||||
PlayerMovementController>(runtime.MovementOwner.Controller);
|
||||
|
|
@ -1010,6 +1153,197 @@ public sealed class HeadlessSessionHostTests
|
|||
|
||||
private sealed class FixtureCollisionPublicationException : Exception;
|
||||
|
||||
[Fact]
|
||||
public void MissingPreparedCollisionYieldsTypedRetryAndCompletesWhenAvailable()
|
||||
{
|
||||
// C3c contract: the prepared-collision read failure is the
|
||||
// conductor's typed AwaitingCollisionSource retry — no
|
||||
// InvalidDataException (or any exception) escapes the host wiring,
|
||||
// the entity stays a tracked, re-drivable first entry, and the same
|
||||
// sequence completes once the source can serve the Setup.
|
||||
var operations = new FixtureSessionOperations();
|
||||
using var credential = new HeadlessCredentialSecret(
|
||||
"fixture",
|
||||
"password");
|
||||
using var host = new HeadlessSessionHost(
|
||||
Descriptor(),
|
||||
credential,
|
||||
new HeadlessDiagnosticWriter(TextWriter.Null),
|
||||
operations);
|
||||
GameRuntime runtime = host.Runtime;
|
||||
Assert.Equal(
|
||||
RuntimeSessionStartStatus.Connected,
|
||||
host.Start().Status);
|
||||
const uint player = 0x50000021u;
|
||||
runtime.PlayerIdentity.ServerGuid = player;
|
||||
runtime.EntityObjects.Physics.SetPosition.BeginCollisionGeneration(
|
||||
0xA9B40000u, 1UL);
|
||||
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
|
||||
runtime.EntityObjects.Physics.SetPosition.CommitCollisionGeneration(
|
||||
0xA9B40000u, 1UL, ready: true);
|
||||
var source = new FlakySetupCollisionSource();
|
||||
var firstEntry = new AcDream.Runtime.Session
|
||||
.RuntimeFirstEntryDriveController(
|
||||
runtime.EntityObjects,
|
||||
runtime.Clock,
|
||||
source,
|
||||
() => PlayerMovementConstructionOptions.From(
|
||||
runtime.CharacterOwner.MovementSkills.Snapshot),
|
||||
static _ => new RuntimeLocalPlayerPhysicsActivationPreparation(
|
||||
Radius: 0.48f,
|
||||
Height: 1.835f,
|
||||
RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
|
||||
RuntimeEntityRecord record = runtime.EntityObjects
|
||||
.RegisterEntityWithInitialResidence(Spawn(player), isLocalPlayer: true)
|
||||
.Canonical!;
|
||||
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
|
||||
record,
|
||||
record.CreateIntegrationVersion,
|
||||
record.Snapshot,
|
||||
replaceGeneration: false));
|
||||
var collision = new FixtureCollisionNeighborhood();
|
||||
var projection = new HeadlessSessionWorldProjection(
|
||||
runtime,
|
||||
collision,
|
||||
firstEntry);
|
||||
|
||||
projection.ProjectSpawn(record, isLocalPlayer: true);
|
||||
|
||||
Assert.True(source.SetupReadAttempts >= 1);
|
||||
Assert.Null(runtime.MovementOwner.Controller);
|
||||
Assert.Equal(1, firstEntry.PendingCount);
|
||||
Assert.Equal(0u, record.FullCellId);
|
||||
|
||||
source.Available = true;
|
||||
// The session tick's retry pump.
|
||||
firstEntry.DriveAll();
|
||||
|
||||
Assert.IsType<PlayerMovementController>(
|
||||
runtime.MovementOwner.Controller);
|
||||
Assert.Equal(0, firstEntry.PendingCount);
|
||||
Assert.Equal(0xA9B40000u, record.FullCellId & 0xFFFF0000u);
|
||||
Assert.NotEqual(0u, record.FullCellId);
|
||||
}
|
||||
|
||||
private sealed class FlakySetupCollisionSource
|
||||
: AcDream.Content.IPreparedCollisionSource
|
||||
{
|
||||
internal bool Available { get; set; }
|
||||
internal int SetupReadAttempts { get; private set; }
|
||||
|
||||
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)
|
||||
{
|
||||
SetupReadAttempts++;
|
||||
if (!Available)
|
||||
{
|
||||
return AcDream.Content.PreparedCollisionReadResult<
|
||||
FlatSetupCollision>.Missing;
|
||||
}
|
||||
return AcDream.Content.PreparedCollisionReadResult<
|
||||
FlatSetupCollision>.Loaded(new FlatSetupCollision(
|
||||
System.Collections.Immutable.ImmutableArray<
|
||||
FlatCollisionCylinder>.Empty,
|
||||
[new FlatCollisionSphere(Vector3.Zero, 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static AcDream.Runtime.Session.RuntimeFirstEntryDriveController
|
||||
CreateFirstEntryDrive(GameRuntime runtime) => new(
|
||||
runtime.EntityObjects,
|
||||
runtime.Clock,
|
||||
new LoadedSetupCollisionSource(),
|
||||
() => PlayerMovementConstructionOptions.From(
|
||||
runtime.CharacterOwner.MovementSkills.Snapshot),
|
||||
static _ => new RuntimeLocalPlayerPhysicsActivationPreparation(
|
||||
Radius: 0.48f,
|
||||
Height: 1.835f,
|
||||
RuntimeLocalPlayerShadowDisposition.ProvenShapeless));
|
||||
|
||||
private sealed class LoadedSetupCollisionSource
|
||||
: 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(Vector3.Zero, 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddFlatLandblock(PhysicsEngine engine)
|
||||
{
|
||||
var heights = new byte[81];
|
||||
|
|
@ -1026,10 +1360,12 @@ public sealed class HeadlessSessionHostTests
|
|||
worldOffsetY: 0f);
|
||||
}
|
||||
|
||||
private static WorldSession.EntitySpawn Spawn(uint guid)
|
||||
private static WorldSession.EntitySpawn Spawn(
|
||||
uint guid,
|
||||
uint cellId = 0xA9B40001u)
|
||||
{
|
||||
var position = new CreateObject.ServerPosition(
|
||||
0xA9B40001u,
|
||||
cellId,
|
||||
96f,
|
||||
97f,
|
||||
50f,
|
||||
|
|
@ -1217,6 +1553,22 @@ public sealed class HeadlessSessionHostTests
|
|||
|
||||
public bool IsReady(uint fullCellId) =>
|
||||
fullCellId == LastCell;
|
||||
|
||||
// C3c-R1 review F7: the fixture window mirrors production's 3x3
|
||||
// membership around the last requested center; no center yet means
|
||||
// "within" (never convert before the first CenterOn).
|
||||
public bool IsWithinServiceWindow(uint fullCellId)
|
||||
{
|
||||
if (LastCell == 0u)
|
||||
return true;
|
||||
int dx = Math.Abs(
|
||||
(int)((fullCellId >> 24) & 0xFFu)
|
||||
- (int)((LastCell >> 24) & 0xFFu));
|
||||
int dy = Math.Abs(
|
||||
(int)((fullCellId >> 16) & 0xFFu)
|
||||
- (int)((LastCell >> 16) & 0xFFu));
|
||||
return dx <= 1 && dy <= 1;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FixtureEventRoute(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue