Own render, physics, DAT-static presentation, and retained static-resource rebinds behind retryable receipts so a failed publication resumes its exact unfinished suffix without replaying retail create-time defaults. Co-authored-by: Codex <noreply@openai.com>
712 lines
26 KiB
C#
712 lines
26 KiB
C#
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)
|
|
&& method.ReturnType == typeof(LandblockPhysicsPublication));
|
|
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.");
|
|
}
|
|
}
|