acdream/tests/AcDream.App.Tests/Streaming/LandblockPhysicsPublisherTests.cs
Erik ea0da8c8ae refactor(streaming): compose landblock presentation transaction
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>
2026-07-21 21:45:16 +02:00

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.");
}
}