refactor(streaming): extract landblock physics publisher
Move streamed terrain/cell/building and static collision publication behind a focused update-thread owner. Preserve retail publication order while making replacement and retirement exact by logical landblock ownership, including current-cell rebasing and adjacent-seam isolation. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
acb6b34d01
commit
3613d393e6
7 changed files with 1538 additions and 546 deletions
|
|
@ -0,0 +1,711 @@
|
|||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.App.Tests.Streaming;
|
||||
|
||||
public sealed class LandblockPhysicsPublisherTests
|
||||
{
|
||||
private const uint FirstLandblock = 0xA9B4FFFFu;
|
||||
private const uint AdjacentLandblock = 0xAAB4FFFFu;
|
||||
private const uint SetupId = 0x02000042u;
|
||||
private const uint SecondSetupId = 0x02000043u;
|
||||
private static readonly float[] HeightTable =
|
||||
Enumerable.Range(0, 256).Select(index => (float)index).ToArray();
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ClonesHeightTableAndRejectsIncompleteInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new LandblockPhysicsPublisher(
|
||||
new PhysicsEngine(),
|
||||
new float[255]));
|
||||
Assert.Throws<ArgumentException>(() => new LandblockPhysicsPublisher(
|
||||
new PhysicsEngine(),
|
||||
HeightTable));
|
||||
|
||||
float[] mutable = HeightTable.ToArray();
|
||||
var engine = new PhysicsEngine();
|
||||
var cache = new PhysicsDataCache();
|
||||
engine.DataCache = cache;
|
||||
var publisher = new LandblockPhysicsPublisher(engine, mutable);
|
||||
mutable[0] = 999f;
|
||||
|
||||
LandblockPhysicsPublication receipt = Begin(
|
||||
publisher,
|
||||
Build(FirstLandblock));
|
||||
publisher.CompletePublication(receipt);
|
||||
|
||||
Assert.Equal(1, engine.LandblockCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FarPublication_IsTerrainOnlyAndDemotionPreservesIt()
|
||||
{
|
||||
var fixture = Fixture();
|
||||
LandblockPhysicsPublication receipt = Begin(
|
||||
fixture.Publisher,
|
||||
Build(FirstLandblock, bundle: PhysicsDatBundle.Empty));
|
||||
fixture.Publisher.CompletePublication(receipt);
|
||||
|
||||
Assert.Equal(1, fixture.Engine.LandblockCount);
|
||||
Assert.True(fixture.Engine.IsLandblockTerrainResident(FirstLandblock));
|
||||
Assert.Equal(0, fixture.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||
|
||||
fixture.Publisher.DemoteToTerrain(FirstLandblock);
|
||||
|
||||
Assert.Equal(1, fixture.Engine.LandblockCount);
|
||||
Assert.True(fixture.Engine.IsLandblockTerrainResident(FirstLandblock));
|
||||
Assert.Equal(1, fixture.Publisher.Diagnostics.DemotionCount);
|
||||
|
||||
fixture.Publisher.RemoveLandblock(FirstLandblock);
|
||||
Assert.Equal(0, fixture.Engine.LandblockCount);
|
||||
Assert.False(fixture.Engine.IsLandblockTerrainResident(FirstLandblock));
|
||||
Assert.Equal(1, fixture.Publisher.Diagnostics.FullRemovalCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BeginPublication_CommitsCellPortalAndBuildingPhysicsFromOneBundle()
|
||||
{
|
||||
var fixture = Fixture();
|
||||
const uint envCellId = 0xA9B40100u;
|
||||
PhysicsDatBundle bundle = CellPortalAndBuildingBundle(envCellId);
|
||||
|
||||
LandblockPhysicsPublication receipt = Begin(
|
||||
fixture.Publisher,
|
||||
Build(
|
||||
FirstLandblock,
|
||||
bundle: bundle,
|
||||
origin: new LandblockBuildOrigin(0xA8, 0xB4)));
|
||||
|
||||
Assert.Equal(new Vector3(192f, 0f, 0f), receipt.Origin);
|
||||
LandblockPhysicsPublisherDiagnostics diagnostics =
|
||||
fixture.Publisher.Diagnostics;
|
||||
Assert.Equal(1, diagnostics.CellSurfaceCount);
|
||||
Assert.Equal(1, diagnostics.PortalPlaneCount);
|
||||
Assert.Equal(1, diagnostics.BuildingCount);
|
||||
Assert.NotNull(fixture.Cache.CellGraph.GetVisible(envCellId));
|
||||
BuildingPhysics building = Assert.Single(
|
||||
fixture.Cache.BuildingIds.Select(id => fixture.Cache.GetBuilding(id)!));
|
||||
Assert.Equal(new Vector3(204f, 12f, 0f), building.WorldTransform.Translation);
|
||||
Assert.Equal(0x01000077u, building.ModelId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompletePublication_PhysicsBspSuppressesSetupCylinderFallback()
|
||||
{
|
||||
const uint gfxObjId = 0x01000077u;
|
||||
var fixture = Fixture();
|
||||
CacheCylinderSetup(fixture.Cache);
|
||||
fixture.Cache.RegisterGfxObjForTest(gfxObjId, BspGfx(radius: 1.25f));
|
||||
WorldEntity entity = new()
|
||||
{
|
||||
Id = 0x80A9B401u,
|
||||
SourceGfxObjOrSetupId = SetupId,
|
||||
Position = new Vector3(12f, 12f, 0f),
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = [new MeshRef(gfxObjId, Matrix4x4.Identity)],
|
||||
};
|
||||
|
||||
Publish(fixture.Publisher, Build(FirstLandblock, [entity]));
|
||||
|
||||
ShadowEntry entry = Assert.Single(
|
||||
fixture.Engine.ShadowObjects.AllEntriesForDebug());
|
||||
Assert.Equal(ShadowCollisionType.BSP, entry.CollisionType);
|
||||
Assert.Equal(1, fixture.Publisher.Diagnostics.StaticBspOwnerCount);
|
||||
Assert.Equal(0, fixture.Publisher.Diagnostics.StaticCylinderOwnerCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompletePublication_BuildingShellNeverRegistersSetupCollision()
|
||||
{
|
||||
var fixture = Fixture();
|
||||
CacheCylinderSetup(fixture.Cache);
|
||||
WorldEntity shell = new()
|
||||
{
|
||||
Id = 0xC0A9B401u,
|
||||
SourceGfxObjOrSetupId = SetupId,
|
||||
Position = new Vector3(12f, 12f, 0f),
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
IsBuildingShell = true,
|
||||
};
|
||||
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(
|
||||
FirstLandblock,
|
||||
[shell],
|
||||
CellPortalAndBuildingBundle(0xA9B40100u)));
|
||||
|
||||
Assert.Equal(0, fixture.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||
Assert.Empty(fixture.Engine.ShadowObjects.AllEntriesForDebug());
|
||||
Assert.Single(fixture.Cache.BuildingIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompletePublication_MultipleSetupShapesShareOneLogicalOwner()
|
||||
{
|
||||
var fixture = Fixture();
|
||||
CacheCylinderSetup(fixture.Cache, SetupId, shapeCount: 5);
|
||||
CacheCylinderSetup(fixture.Cache, SecondSetupId);
|
||||
const uint firstId = 0x80A9B401u;
|
||||
const uint collidingLegacyId = 0xC0A9B401u;
|
||||
WorldEntity first = CylinderEntity(
|
||||
firstId,
|
||||
new Vector3(12f, 12f, 0f));
|
||||
WorldEntity second = CylinderEntity(
|
||||
collidingLegacyId,
|
||||
new Vector3(36f, 12f, 0f),
|
||||
SecondSetupId);
|
||||
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(FirstLandblock, [first, second]));
|
||||
|
||||
Assert.Equal(2, fixture.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||
ShadowEntry[] entries = fixture.Engine.ShadowObjects
|
||||
.AllEntriesForDebug()
|
||||
.ToArray();
|
||||
Assert.Equal(6, entries.Length);
|
||||
Assert.Equal(
|
||||
[firstId, collidingLegacyId],
|
||||
entries.Select(entry => entry.EntityId).Distinct().Order().ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompletePublication_RunsPerEntityCallbackBeforeCollisionAndIsIdempotent()
|
||||
{
|
||||
var fixture = Fixture();
|
||||
WorldEntity entity = CylinderEntity(
|
||||
0x80A9B401u,
|
||||
new Vector3(12f, 12f, 0f));
|
||||
CacheCylinderSetup(fixture.Cache);
|
||||
LandblockPhysicsPublication receipt = Begin(
|
||||
fixture.Publisher,
|
||||
Build(FirstLandblock, [entity]));
|
||||
int callbackCount = 0;
|
||||
|
||||
fixture.Publisher.CompletePublication(receipt, observed =>
|
||||
{
|
||||
Assert.Same(entity, observed);
|
||||
Assert.Equal(0, fixture.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||
callbackCount++;
|
||||
});
|
||||
fixture.Publisher.CompletePublication(receipt, _ => callbackCount++);
|
||||
|
||||
Assert.Equal(1, callbackCount);
|
||||
Assert.Equal(1, fixture.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||
ShadowEntry entry = Assert.Single(
|
||||
fixture.Engine.ShadowObjects.AllEntriesForDebug());
|
||||
Assert.Equal(entity.Id, entry.EntityId);
|
||||
Assert.Equal(ShadowCollisionType.Cylinder, entry.CollisionType);
|
||||
Assert.Equal(1, fixture.Publisher.Diagnostics.CompleteCount);
|
||||
Assert.Equal(1, fixture.Publisher.Diagnostics.RefloodCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompletePublication_CallbackFailureCanRetryWithoutStackingCollision()
|
||||
{
|
||||
var fixture = Fixture();
|
||||
WorldEntity entity = CylinderEntity(
|
||||
0x80A9B401u,
|
||||
new Vector3(12f, 12f, 0f));
|
||||
CacheCylinderSetup(fixture.Cache);
|
||||
LandblockPhysicsPublication receipt = Begin(
|
||||
fixture.Publisher,
|
||||
Build(FirstLandblock, [entity]));
|
||||
int attempts = 0;
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
fixture.Publisher.CompletePublication(receipt, _ =>
|
||||
{
|
||||
attempts++;
|
||||
throw new InvalidOperationException("injected static presentation failure");
|
||||
}));
|
||||
fixture.Publisher.CompletePublication(receipt, _ => attempts++);
|
||||
|
||||
Assert.Equal(2, attempts);
|
||||
Assert.Equal(1, fixture.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||
Assert.Single(fixture.Engine.ShadowObjects.AllEntriesForDebug());
|
||||
Assert.Equal(1, fixture.Publisher.Diagnostics.CompleteCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NearReapply_ReplacesSameStaticOwnerWithoutDuplication()
|
||||
{
|
||||
var fixture = Fixture();
|
||||
WorldEntity first = CylinderEntity(
|
||||
0x80A9B401u,
|
||||
new Vector3(12f, 12f, 0f));
|
||||
WorldEntity moved = CylinderEntity(
|
||||
first.Id,
|
||||
new Vector3(36f, 12f, 0f));
|
||||
CacheCylinderSetup(fixture.Cache);
|
||||
|
||||
LandblockPhysicsPublication firstReceipt = Begin(
|
||||
fixture.Publisher,
|
||||
Build(FirstLandblock, [first]));
|
||||
fixture.Publisher.CompletePublication(firstReceipt);
|
||||
LandblockPhysicsPublication secondReceipt = Begin(
|
||||
fixture.Publisher,
|
||||
Build(FirstLandblock, [moved]));
|
||||
fixture.Publisher.CompletePublication(secondReceipt);
|
||||
|
||||
Assert.Equal(1, fixture.Engine.LandblockCount);
|
||||
Assert.Equal(1, fixture.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||
ShadowEntry entry = Assert.Single(
|
||||
fixture.Engine.ShadowObjects.AllEntriesForDebug());
|
||||
Assert.Equal(moved.Position + new Vector3(0f, 0f, 0.6f), entry.Position);
|
||||
Assert.Equal(2, fixture.Publisher.Diagnostics.BeginCount);
|
||||
Assert.Equal(2, fixture.Publisher.Diagnostics.CompleteCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NearReapply_RemovesOmittedStaticAcrossSeamAndPreservesNeighborOwner()
|
||||
{
|
||||
var fixture = Fixture();
|
||||
CacheCylinderSetup(fixture.Cache);
|
||||
WorldEntity omitted = CylinderEntity(
|
||||
0x80A9B401u,
|
||||
new Vector3(191.5f, 12f, 0f));
|
||||
WorldEntity neighbor = CylinderEntity(
|
||||
0x80AAB401u,
|
||||
new Vector3(192.5f, 12f, 0f));
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(FirstLandblock, [omitted]));
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(AdjacentLandblock, [neighbor]));
|
||||
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(FirstLandblock, Array.Empty<WorldEntity>()));
|
||||
|
||||
Assert.Equal(1, fixture.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||
ShadowEntry survivor = Assert.Single(
|
||||
fixture.Engine.ShadowObjects.AllEntriesForDebug());
|
||||
Assert.Equal(neighbor.Id, survivor.EntityId);
|
||||
Assert.Contains(
|
||||
fixture.Engine.ShadowObjects.GetObjectsInCell(0xAAB40001u),
|
||||
entry => entry.EntityId == neighbor.Id);
|
||||
Assert.DoesNotContain(
|
||||
fixture.Engine.ShadowObjects.AllEntriesForDebug(),
|
||||
entry => entry.EntityId == omitted.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NearReapply_RebasesCellAndBuildingAndRemovesMissingSnapshotData()
|
||||
{
|
||||
const uint envCellId = 0xA9B40100u;
|
||||
var fixture = Fixture();
|
||||
PhysicsDatBundle populated = CellPortalAndBuildingBundle(envCellId);
|
||||
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(FirstLandblock, bundle: populated));
|
||||
fixture.Engine.UpdatePlayerCurrCell(envCellId);
|
||||
AcDream.Core.World.Cells.ObjCell originalCurrent =
|
||||
fixture.Cache.CellGraph.CurrCell!;
|
||||
Assert.Equal(
|
||||
Vector3.Zero,
|
||||
fixture.Cache.CellGraph.GetVisible(envCellId)!.WorldTransform.Translation);
|
||||
Assert.Equal(
|
||||
new Vector3(12f, 12f, 0f),
|
||||
Assert.Single(fixture.Cache.BuildingIds
|
||||
.Select(id => fixture.Cache.GetBuilding(id)!))
|
||||
.WorldTransform.Translation);
|
||||
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(
|
||||
FirstLandblock,
|
||||
bundle: populated,
|
||||
origin: new LandblockBuildOrigin(0xA8, 0xB4)));
|
||||
Assert.Equal(
|
||||
new Vector3(192f, 0f, 0f),
|
||||
fixture.Cache.CellGraph.GetVisible(envCellId)!.WorldTransform.Translation);
|
||||
Assert.NotSame(originalCurrent, fixture.Cache.CellGraph.CurrCell);
|
||||
Assert.Same(
|
||||
fixture.Cache.CellGraph.GetVisible(envCellId),
|
||||
fixture.Cache.CellGraph.CurrCell);
|
||||
Assert.Equal(
|
||||
new Vector3(204f, 12f, 0f),
|
||||
Assert.Single(fixture.Cache.BuildingIds
|
||||
.Select(id => fixture.Cache.GetBuilding(id)!))
|
||||
.WorldTransform.Translation);
|
||||
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(FirstLandblock, bundle: PhysicsDatBundle.Empty));
|
||||
|
||||
Assert.Null(fixture.Cache.CellGraph.GetVisible(envCellId));
|
||||
Assert.Null(fixture.Cache.CellGraph.CurrCell);
|
||||
Assert.Empty(fixture.Cache.BuildingIds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NearReapply_DoesNotRetireAdjacentCellOrBuildingCache()
|
||||
{
|
||||
const uint firstCellId = 0xA9B40100u;
|
||||
const uint adjacentCellId = 0xAAB40100u;
|
||||
var fixture = Fixture();
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(
|
||||
FirstLandblock,
|
||||
bundle: CellPortalAndBuildingBundle(firstCellId)));
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(
|
||||
AdjacentLandblock,
|
||||
bundle: CellPortalAndBuildingBundle(adjacentCellId)));
|
||||
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(FirstLandblock, bundle: PhysicsDatBundle.Empty));
|
||||
|
||||
Assert.Null(fixture.Cache.CellGraph.GetVisible(firstCellId));
|
||||
Assert.NotNull(fixture.Cache.CellGraph.GetVisible(adjacentCellId));
|
||||
Assert.DoesNotContain(
|
||||
fixture.Cache.BuildingIds,
|
||||
id => (id & 0xFFFF0000u) == 0xA9B40000u);
|
||||
Assert.Contains(
|
||||
fixture.Cache.BuildingIds,
|
||||
id => (id & 0xFFFF0000u) == 0xAAB40000u);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdjacentLandblockDemotion_DoesNotEraseNeighborStaticOwner()
|
||||
{
|
||||
var fixture = Fixture();
|
||||
CacheCylinderSetup(fixture.Cache);
|
||||
WorldEntity first = CylinderEntity(
|
||||
0x80A9B401u,
|
||||
new Vector3(191.5f, 12f, 0f));
|
||||
WorldEntity neighbor = CylinderEntity(
|
||||
0x80AAB401u,
|
||||
new Vector3(192.5f, 12f, 0f));
|
||||
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(FirstLandblock, [first]));
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(AdjacentLandblock, [neighbor]));
|
||||
Assert.Equal(2, fixture.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||
Assert.Contains(
|
||||
fixture.Engine.ShadowObjects.GetObjectsInCell(0xAAB40001u),
|
||||
entry => entry.EntityId == neighbor.Id);
|
||||
|
||||
fixture.Publisher.DemoteToTerrain(FirstLandblock);
|
||||
|
||||
Assert.Equal(1, fixture.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||
ShadowEntry surviving = Assert.Single(
|
||||
fixture.Engine.ShadowObjects.AllEntriesForDebug());
|
||||
Assert.Equal(neighbor.Id, surviving.EntityId);
|
||||
Assert.True(fixture.Engine.IsLandblockTerrainResident(FirstLandblock));
|
||||
Assert.True(fixture.Engine.IsLandblockTerrainResident(AdjacentLandblock));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullRemoval_DoesNotEraseAdjacentLandblockCollision()
|
||||
{
|
||||
var fixture = Fixture();
|
||||
CacheCylinderSetup(fixture.Cache);
|
||||
WorldEntity first = CylinderEntity(
|
||||
0x80A9B401u,
|
||||
new Vector3(191.5f, 12f, 0f));
|
||||
WorldEntity neighbor = CylinderEntity(
|
||||
0x80AAB401u,
|
||||
new Vector3(192.5f, 12f, 0f));
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(
|
||||
FirstLandblock,
|
||||
[first],
|
||||
CellPortalAndBuildingBundle(0xA9B40100u)));
|
||||
Publish(
|
||||
fixture.Publisher,
|
||||
Build(
|
||||
AdjacentLandblock,
|
||||
[neighbor],
|
||||
CellPortalAndBuildingBundle(0xAAB40100u)));
|
||||
|
||||
fixture.Publisher.RemoveLandblock(FirstLandblock);
|
||||
|
||||
Assert.Equal(1, fixture.Engine.LandblockCount);
|
||||
Assert.False(fixture.Engine.IsLandblockTerrainResident(FirstLandblock));
|
||||
Assert.True(fixture.Engine.IsLandblockTerrainResident(AdjacentLandblock));
|
||||
Assert.Equal(1, fixture.Engine.ShadowObjects.RetainedRegistrationCount);
|
||||
Assert.Equal(
|
||||
neighbor.Id,
|
||||
Assert.Single(fixture.Engine.ShadowObjects.AllEntriesForDebug()).EntityId);
|
||||
uint survivingBuilding = Assert.Single(fixture.Cache.BuildingIds);
|
||||
Assert.Equal(0xAAB40000u, survivingBuilding & 0xFFFF0000u);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CompletePublication_ForeignReceiptIsRejectedWithoutConsumingIt()
|
||||
{
|
||||
var first = Fixture();
|
||||
var second = Fixture();
|
||||
LandblockPhysicsPublication receipt = Begin(
|
||||
first.Publisher,
|
||||
Build(FirstLandblock));
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
second.Publisher.CompletePublication(receipt));
|
||||
Assert.Equal(0, second.Publisher.Diagnostics.CompleteCount);
|
||||
|
||||
first.Publisher.CompletePublication(receipt);
|
||||
Assert.Equal(1, first.Publisher.Diagnostics.CompleteCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublisherSurface_HasNoDatReaderResidencyOrLiveOriginDependency()
|
||||
{
|
||||
Type type = typeof(LandblockPhysicsPublisher);
|
||||
Type[] dependencies = type
|
||||
.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
|
||||
.Select(field => field.FieldType)
|
||||
.Concat(type.GetConstructors().SelectMany(constructor =>
|
||||
constructor.GetParameters().Select(parameter => parameter.ParameterType)))
|
||||
.ToArray();
|
||||
|
||||
Assert.DoesNotContain(dependencies, dependency =>
|
||||
dependency.FullName?.Contains("DatReader", StringComparison.Ordinal) == true);
|
||||
Assert.DoesNotContain(dependencies, dependency =>
|
||||
dependency.FullName?.Contains("StreamingRegion", StringComparison.Ordinal) == true);
|
||||
Assert.DoesNotContain(dependencies, dependency =>
|
||||
dependency.FullName?.Contains("LiveWorldOrigin", StringComparison.Ordinal) == true);
|
||||
|
||||
MethodInfo begin = Assert.Single(
|
||||
type.GetMethods(),
|
||||
method => method.Name == nameof(LandblockPhysicsPublisher.BeginPublication));
|
||||
ParameterInfo parameter = Assert.Single(begin.GetParameters());
|
||||
Assert.Equal(typeof(LandblockRenderPublication), parameter.ParameterType);
|
||||
Assert.DoesNotContain(type.GetConstructors().SelectMany(constructor =>
|
||||
constructor.GetParameters()), parameterInfo =>
|
||||
parameterInfo.ParameterType == typeof(PhysicsDataCache));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GameWindow_HasNoLandblockPhysicsPublicationBodies()
|
||||
{
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
FindRepoRoot(),
|
||||
"src",
|
||||
"AcDream.App",
|
||||
"Rendering",
|
||||
"GameWindow.cs"));
|
||||
|
||||
Assert.DoesNotContain("_physicsDataCache.CacheCellStruct", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_physicsDataCache.CacheBuilding", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("ShadowShapeBuilder.FromLandblockBspParts", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("ShadowObjects.RefloodLandblock", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_physicsEngine.DemoteLandblockToTerrain", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_physicsEngine.RemoveLandblock", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static void Publish(
|
||||
LandblockPhysicsPublisher publisher,
|
||||
LandblockBuild build)
|
||||
{
|
||||
LandblockPhysicsPublication receipt = Begin(publisher, build);
|
||||
publisher.CompletePublication(receipt);
|
||||
}
|
||||
|
||||
private static LandblockPhysicsPublication Begin(
|
||||
LandblockPhysicsPublisher publisher,
|
||||
LandblockBuild build) =>
|
||||
publisher.BeginPublication(RenderReceipt(build));
|
||||
|
||||
private static LandblockRenderPublication RenderReceipt(LandblockBuild build)
|
||||
{
|
||||
var publisher = new LandblockRenderPublisher(
|
||||
publishTerrain: (_, _, _) => { },
|
||||
removeTerrain: _ => { },
|
||||
cellVisibility: new AcDream.App.Rendering.CellVisibility(),
|
||||
worldState: new GpuWorldState());
|
||||
return publisher.BeginPublication(
|
||||
build,
|
||||
new AcDream.Core.Terrain.LandblockMeshData(
|
||||
Array.Empty<AcDream.Core.Terrain.TerrainVertex>(),
|
||||
Array.Empty<uint>()));
|
||||
}
|
||||
|
||||
private static (
|
||||
LandblockPhysicsPublisher Publisher,
|
||||
PhysicsEngine Engine,
|
||||
PhysicsDataCache Cache) Fixture()
|
||||
{
|
||||
var engine = new PhysicsEngine();
|
||||
var cache = new PhysicsDataCache();
|
||||
engine.DataCache = cache;
|
||||
return (
|
||||
new LandblockPhysicsPublisher(engine, HeightTable),
|
||||
engine,
|
||||
cache);
|
||||
}
|
||||
|
||||
private static void CacheCylinderSetup(
|
||||
PhysicsDataCache cache,
|
||||
uint setupId = SetupId,
|
||||
int shapeCount = 1)
|
||||
{
|
||||
var setup = new Setup();
|
||||
for (int index = 0; index < shapeCount; index++)
|
||||
{
|
||||
setup.CylSpheres.Add(
|
||||
new CylSphere
|
||||
{
|
||||
Radius = 0.4f + index * 0.01f,
|
||||
Height = 1.2f,
|
||||
Origin = new Vector3(index * 0.1f, 0f, 0.6f),
|
||||
});
|
||||
}
|
||||
cache.CacheSetup(setupId, setup);
|
||||
}
|
||||
|
||||
private static GfxObjPhysics BspGfx(float radius) => new()
|
||||
{
|
||||
BSP = new PhysicsBSPTree
|
||||
{
|
||||
Root = new PhysicsBSPNode
|
||||
{
|
||||
Type = DatReaderWriter.Enums.BSPNodeType.Leaf,
|
||||
},
|
||||
},
|
||||
BoundingSphere = new Sphere
|
||||
{
|
||||
Origin = Vector3.Zero,
|
||||
Radius = radius,
|
||||
},
|
||||
PhysicsPolygons = new Dictionary<ushort, Polygon>(),
|
||||
Vertices = new VertexArray(),
|
||||
Resolved = new Dictionary<ushort, ResolvedPolygon>(),
|
||||
};
|
||||
|
||||
private static PhysicsDatBundle CellPortalAndBuildingBundle(uint envCellId)
|
||||
{
|
||||
var cellStruct = new CellStruct
|
||||
{
|
||||
VertexArray = new VertexArray
|
||||
{
|
||||
Vertices = new Dictionary<ushort, SWVertex>
|
||||
{
|
||||
[0] = new SWVertex { Origin = Vector3.Zero },
|
||||
[1] = new SWVertex { Origin = Vector3.UnitY },
|
||||
[2] = new SWVertex { Origin = Vector3.UnitZ },
|
||||
},
|
||||
},
|
||||
PhysicsPolygons = new Dictionary<ushort, Polygon>(),
|
||||
Polygons = new Dictionary<ushort, Polygon>
|
||||
{
|
||||
[0] = new Polygon { VertexIds = [0, 1, 2] },
|
||||
},
|
||||
};
|
||||
var environment = new DatReaderWriter.DBObjs.Environment
|
||||
{
|
||||
Id = 0x0D000001u,
|
||||
Cells = { [1] = cellStruct },
|
||||
};
|
||||
var envCell = new EnvCell
|
||||
{
|
||||
Id = envCellId,
|
||||
EnvironmentId = 1,
|
||||
CellStructure = 1,
|
||||
Position = new Frame { Orientation = Quaternion.Identity },
|
||||
CellPortals =
|
||||
{
|
||||
new CellPortal
|
||||
{
|
||||
PolygonId = 0,
|
||||
OtherCellId = 0xFFFF,
|
||||
},
|
||||
},
|
||||
};
|
||||
var info = new LandBlockInfo
|
||||
{
|
||||
NumCells = 1,
|
||||
Buildings =
|
||||
{
|
||||
new BuildingInfo
|
||||
{
|
||||
ModelId = 0x01000077u,
|
||||
Frame = new Frame
|
||||
{
|
||||
Origin = new Vector3(12f, 12f, 0f),
|
||||
Orientation = Quaternion.Identity,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return new PhysicsDatBundle(
|
||||
info,
|
||||
new Dictionary<uint, EnvCell> { [envCellId] = envCell },
|
||||
new Dictionary<uint, DatReaderWriter.DBObjs.Environment>
|
||||
{
|
||||
[environment.Id] = environment,
|
||||
},
|
||||
new Dictionary<uint, Setup>(),
|
||||
new Dictionary<uint, GfxObj>());
|
||||
}
|
||||
|
||||
private static WorldEntity CylinderEntity(
|
||||
uint id,
|
||||
Vector3 position,
|
||||
uint setupId = SetupId) => new()
|
||||
{
|
||||
Id = id,
|
||||
SourceGfxObjOrSetupId = setupId,
|
||||
Position = position,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
|
||||
private static LandblockBuild Build(
|
||||
uint landblockId,
|
||||
IReadOnlyList<WorldEntity>? entities = null,
|
||||
PhysicsDatBundle? bundle = null,
|
||||
LandblockBuildOrigin? origin = null)
|
||||
{
|
||||
var landblockInfo = new LandBlockInfo();
|
||||
PhysicsDatBundle physics = bundle ?? new PhysicsDatBundle(
|
||||
landblockInfo,
|
||||
new Dictionary<uint, EnvCell>(),
|
||||
new Dictionary<uint, DatReaderWriter.DBObjs.Environment>(),
|
||||
new Dictionary<uint, Setup>(),
|
||||
new Dictionary<uint, GfxObj>());
|
||||
return new LandblockBuild(
|
||||
new LoadedLandblock(
|
||||
landblockId,
|
||||
FlatHeightmap(),
|
||||
entities ?? Array.Empty<WorldEntity>(),
|
||||
physics),
|
||||
Origin: origin ?? new LandblockBuildOrigin(0xA9, 0xB4));
|
||||
}
|
||||
|
||||
private static LandBlock FlatHeightmap() => new()
|
||||
{
|
||||
Terrain = new TerrainInfo[81],
|
||||
Height = new byte[81],
|
||||
};
|
||||
|
||||
private static string FindRepoRoot()
|
||||
{
|
||||
string? directory = AppContext.BaseDirectory;
|
||||
while (directory is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(directory, "AcDream.slnx")))
|
||||
return directory;
|
||||
directory = Directory.GetParent(directory)?.FullName;
|
||||
}
|
||||
throw new DirectoryNotFoundException("Could not locate repository root.");
|
||||
}
|
||||
}
|
||||
|
|
@ -151,4 +151,36 @@ public class PhysicsEngineResidencyTests
|
|||
entry => entry.EntityId == dynamicId);
|
||||
Assert.Equal(1, engine.ShadowObjects.RetainedRegistrationCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveLandblock_RemovesOnlyItsCachedBuildings()
|
||||
{
|
||||
const uint landblockId = 0xA9B4FFFFu;
|
||||
const uint retiredBuildingCell = 0xA9B40001u;
|
||||
const uint adjacentBuildingCell = 0xAAB40001u;
|
||||
var cache = new PhysicsDataCache();
|
||||
var engine = new PhysicsEngine { DataCache = cache };
|
||||
cache.RegisterBuildingForTest(
|
||||
retiredBuildingCell,
|
||||
Building(Matrix4x4.Identity));
|
||||
cache.RegisterBuildingForTest(
|
||||
adjacentBuildingCell,
|
||||
Building(Matrix4x4.CreateTranslation(192f, 0f, 0f)));
|
||||
|
||||
engine.RemoveLandblock(landblockId);
|
||||
|
||||
Assert.Null(cache.GetBuilding(retiredBuildingCell));
|
||||
Assert.NotNull(cache.GetBuilding(adjacentBuildingCell));
|
||||
}
|
||||
|
||||
private static BuildingPhysics Building(Matrix4x4 transform)
|
||||
{
|
||||
Matrix4x4.Invert(transform, out Matrix4x4 inverse);
|
||||
return new BuildingPhysics
|
||||
{
|
||||
WorldTransform = transform,
|
||||
InverseWorldTransform = inverse,
|
||||
Portals = Array.Empty<BldPortalInfo>(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,21 @@ public class CellGraphTests
|
|||
Assert.Null(g.GetVisible(0xA9B40014u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveLandblock_ClearsCurrentCellForRetiredPrefixOnly()
|
||||
{
|
||||
var g = new CellGraph();
|
||||
var current = Env(0xA9B40174u);
|
||||
g.Add(current);
|
||||
g.CurrCell = current;
|
||||
|
||||
g.RemoveLandblock(0xAAB40000u);
|
||||
Assert.Same(current, g.CurrCell);
|
||||
|
||||
g.RemoveLandblock(0xA9B40000u);
|
||||
Assert.Null(g.CurrCell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveEnvCellsForLandblock_PreservesTerrain()
|
||||
{
|
||||
|
|
@ -69,6 +84,21 @@ public class CellGraphTests
|
|||
Assert.IsType<LandCell>(g.GetVisible(0xA9B40014u));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveEnvCellsForLandblock_ClearsOnlyMatchingIndoorCurrentCell()
|
||||
{
|
||||
var g = new CellGraph();
|
||||
var current = Env(0xA9B40174u);
|
||||
g.Add(current);
|
||||
g.CurrCell = current;
|
||||
|
||||
g.RemoveEnvCellsForLandblock(0xAAB4FFFFu);
|
||||
Assert.Same(current, g.CurrCell);
|
||||
|
||||
g.RemoveEnvCellsForLandblock(0xA9B4FFFFu);
|
||||
Assert.Null(g.CurrCell);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Neighbor_ResolvesPortalOtherCellId()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue