feat(headless): hydrate isolated collision worlds

This commit is contained in:
Erik 2026-07-27 09:25:58 +02:00
parent 9569dadb57
commit b6547ff38c
20 changed files with 1960 additions and 101 deletions

View file

@ -13,6 +13,7 @@ using AcDream.Runtime;
using AcDream.Runtime.Entities;
using AcDream.Runtime.Gameplay;
using AcDream.Runtime.Session;
using AcDream.Runtime.World;
namespace AcDream.Headless.Tests;
@ -186,6 +187,71 @@ public sealed class HeadlessSessionHostTests
Assert.True(host.Runtime.CaptureOwnership().IsConverged);
}
[Fact]
public void WorldProjectionHydratesCanonicalMovementAndTeleportState()
{
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;
const uint player = 0x50000002u;
runtime.PlayerIdentity.ServerGuid = player;
AddFlatLandblock(runtime.EntityObjects.Physics.Engine);
RuntimeEntityRecord record = runtime.EntityObjects
.RegisterEntity(Spawn(player))
.Canonical!;
Assert.True(runtime.EntityObjects.ApplyAcceptedSpawn(
record,
record.CreateIntegrationVersion,
record.Snapshot,
replaceGeneration: false));
var collision = new FixtureCollisionNeighborhood();
var projection = new HeadlessSessionWorldProjection(
runtime,
collision);
projection.ProjectSpawn(record, isLocalPlayer: true);
PlayerMovementController controller =
Assert.IsType<PlayerMovementController>(
runtime.MovementOwner.Controller);
projection.ProjectPosition(record, isLocalPlayer: true);
Assert.Same(controller, runtime.MovementOwner.Controller);
Assert.Equal(record.LocalEntityId, controller.LocalEntityId);
Assert.Equal(
0xA9B40000u,
controller.CellId & 0xFFFF0000u);
Assert.True((controller.CellId & 0xFFFFu) < 0x0100u);
projection.BeginTeleport();
Assert.Equal(PlayerState.PortalSpace, controller.State);
RuntimeDestinationReadiness readiness =
projection.PrepareDestination(
revealGeneration: 7,
new RuntimeTeleportDestination(
player,
InstanceSequence: 1,
PositionSequence: 2,
TeleportSequence: 1,
ForcePositionSequence: 0,
new Position(
0xA9B40001u,
new Vector3(96f, 97f, 50f),
Quaternion.Identity)));
Assert.True(readiness.IsCollisionReady);
Assert.False(readiness.IsUnhydratable);
Assert.Equal(PlayerState.InWorld, controller.State);
Assert.Equal(4, collision.CenterCount);
Assert.Equal(0xA9B40001u, collision.LastCell);
}
private static HeadlessSessionDescriptor Descriptor(
HeadlessCredentialProviderKind provider =
HeadlessCredentialProviderKind.Environment,
@ -217,18 +283,7 @@ public sealed class HeadlessSessionHostTests
{
const uint player = 0x50000002u;
PhysicsEngine engine = runtime.EntityObjects.Physics.Engine;
var heights = new byte[81];
Array.Fill(heights, (byte)50);
var heightTable = new float[256];
for (int index = 0; index < heightTable.Length; index++)
heightTable[index] = index;
engine.AddLandblock(
0xA9B4FFFFu,
new TerrainSurface(heights, heightTable),
[],
[],
worldOffsetX: 0f,
worldOffsetY: 0f);
AddFlatLandblock(engine);
RuntimeEntityRecord record = runtime.EntityObjects
.RegisterEntity(Spawn(player))
@ -246,6 +301,22 @@ public sealed class HeadlessSessionHostTests
runtime.MovementOwner.Controller = controller;
}
private static void AddFlatLandblock(PhysicsEngine engine)
{
var heights = new byte[81];
Array.Fill(heights, (byte)50);
var heightTable = new float[256];
for (int index = 0; index < heightTable.Length; index++)
heightTable[index] = index;
engine.AddLandblock(
0xA9B4FFFFu,
new TerrainSurface(heights, heightTable),
[],
[],
worldOffsetX: 0f,
worldOffsetY: 0f);
}
private static WorldSession.EntitySpawn Spawn(uint guid)
{
var position = new CreateObject.ServerPosition(
@ -389,4 +460,20 @@ public sealed class HeadlessSessionHostTests
}
}
private sealed class FixtureCollisionNeighborhood
: IHeadlessCollisionNeighborhood
{
public int CenterCount { get; private set; }
public uint LastCell { get; private set; }
public void CenterOn(uint fullCellId)
{
CenterCount++;
LastCell = fullCellId;
}
public bool IsReady(uint fullCellId) =>
fullCellId == LastCell;
}
}