diff --git a/src/AcDream.Core/World/.gitkeep b/src/AcDream.Core/World/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/AcDream.Core/World/LandblockLoader.cs b/src/AcDream.Core/World/LandblockLoader.cs new file mode 100644 index 0000000..4234c11 --- /dev/null +++ b/src/AcDream.Core/World/LandblockLoader.cs @@ -0,0 +1,77 @@ +using DatReaderWriter; +using DatReaderWriter.DBObjs; + +namespace AcDream.Core.World; + +public static class LandblockLoader +{ + private const uint GfxObjMask = 0x01000000u; + private const uint SetupMask = 0x02000000u; + private const uint TypeMask = 0xFF000000u; + + /// + /// Load a single landblock (heightmap + static objects) from the dats. + /// + /// Null if the landblock is missing from the cell dat. + public static LoadedLandblock? Load(DatCollection dats, uint landblockId) + { + var block = dats.Get(landblockId); + if (block is null) + return null; + + var info = dats.Get((landblockId & 0xFFFF0000u) | 0xFFFEu); + var entities = info is null + ? Array.Empty() + : BuildEntitiesFromInfo(info); + + return new LoadedLandblock(landblockId, block, entities); + } + + /// + /// Pure mapping from a parsed LandBlockInfo to a list of WorldEntity. + /// Each Stab and BuildingInfo becomes one entity. Unsupported id types + /// (neither GfxObj 0x01xxxxxx nor Setup 0x02xxxxxx) are silently skipped. + /// MeshRefs is left empty at this stage — Task 5 populates it. + /// + public static IReadOnlyList BuildEntitiesFromInfo(LandBlockInfo info) + { + var result = new List(info.Objects.Count + info.Buildings.Count); + uint nextId = 1; + + foreach (var stab in info.Objects) + { + if (!IsSupported(stab.Id)) + continue; + result.Add(new WorldEntity + { + Id = nextId++, + SourceGfxObjOrSetupId = stab.Id, + Position = stab.Frame.Origin, + Rotation = stab.Frame.Orientation, + MeshRefs = Array.Empty(), + }); + } + + foreach (var building in info.Buildings) + { + if (!IsSupported(building.ModelId)) + continue; + result.Add(new WorldEntity + { + Id = nextId++, + SourceGfxObjOrSetupId = building.ModelId, + Position = building.Frame.Origin, + Rotation = building.Frame.Orientation, + MeshRefs = Array.Empty(), + }); + } + + return result; + } + + private static bool IsSupported(uint id) + { + var type = id & TypeMask; + return type == GfxObjMask || type == SetupMask; + } +} diff --git a/src/AcDream.Core/World/LoadedLandblock.cs b/src/AcDream.Core/World/LoadedLandblock.cs new file mode 100644 index 0000000..492b1f3 --- /dev/null +++ b/src/AcDream.Core/World/LoadedLandblock.cs @@ -0,0 +1,8 @@ +using DatReaderWriter.DBObjs; + +namespace AcDream.Core.World; + +public sealed record LoadedLandblock( + uint LandblockId, + LandBlock Heightmap, + IReadOnlyList Entities); diff --git a/src/AcDream.Core/World/MeshRef.cs b/src/AcDream.Core/World/MeshRef.cs new file mode 100644 index 0000000..09c3d26 --- /dev/null +++ b/src/AcDream.Core/World/MeshRef.cs @@ -0,0 +1,5 @@ +using System.Numerics; + +namespace AcDream.Core.World; + +public readonly record struct MeshRef(uint GfxObjId, Matrix4x4 PartTransform); diff --git a/src/AcDream.Core/World/WorldEntity.cs b/src/AcDream.Core/World/WorldEntity.cs new file mode 100644 index 0000000..a02e5bf --- /dev/null +++ b/src/AcDream.Core/World/WorldEntity.cs @@ -0,0 +1,12 @@ +using System.Numerics; + +namespace AcDream.Core.World; + +public sealed class WorldEntity +{ + public required uint Id { get; init; } + public required uint SourceGfxObjOrSetupId { get; init; } + public required Vector3 Position { get; init; } + public required Quaternion Rotation { get; init; } + public required IReadOnlyList MeshRefs { get; init; } +} diff --git a/tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs b/tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs new file mode 100644 index 0000000..af68b01 --- /dev/null +++ b/tests/AcDream.Core.Tests/World/LandblockLoaderTests.cs @@ -0,0 +1,119 @@ +using System.Numerics; +using AcDream.Core.World; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Types; + +namespace AcDream.Core.Tests.World; + +public class LandblockLoaderTests +{ + private static LandBlock BuildFlatLandBlock() + { + var block = new LandBlock + { + HasObjects = true, + Terrain = new TerrainInfo[81], + Height = new byte[81], + }; + for (int i = 0; i < 81; i++) + { + block.Terrain[i] = (ushort)0; + block.Height[i] = 0; + } + return block; + } + + [Fact] + public void BuildEntitiesFromInfo_StabsAndBuildings_AreMappedToEntities() + { + var info = new LandBlockInfo + { + Objects = + { + new Stab + { + Id = 0x01000042u, // GfxObj id + Frame = new Frame + { + Origin = new Vector3(10, 20, 5), + Orientation = Quaternion.Identity, + }, + }, + new Stab + { + Id = 0x02000099u, // Setup id + Frame = new Frame + { + Origin = new Vector3(30, 40, 10), + Orientation = Quaternion.Identity, + }, + }, + }, + Buildings = + { + new BuildingInfo + { + ModelId = 0x020000AAu, // Setup for a building + Frame = new Frame + { + Origin = new Vector3(50, 60, 0), + Orientation = Quaternion.Identity, + }, + }, + }, + }; + + var entities = LandblockLoader.BuildEntitiesFromInfo(info); + + Assert.Equal(3, entities.Count); + Assert.Contains(entities, e => e.SourceGfxObjOrSetupId == 0x01000042u && e.Position == new Vector3(10, 20, 5)); + Assert.Contains(entities, e => e.SourceGfxObjOrSetupId == 0x02000099u && e.Position == new Vector3(30, 40, 10)); + Assert.Contains(entities, e => e.SourceGfxObjOrSetupId == 0x020000AAu && e.Position == new Vector3(50, 60, 0)); + } + + [Fact] + public void BuildEntitiesFromInfo_AssignsMonotonicIds() + { + var info = new LandBlockInfo + { + Objects = + { + new Stab { Id = 0x01000001u, Frame = new Frame() }, + new Stab { Id = 0x01000002u, Frame = new Frame() }, + new Stab { Id = 0x01000003u, Frame = new Frame() }, + }, + }; + + var entities = LandblockLoader.BuildEntitiesFromInfo(info); + + var ids = entities.Select(e => e.Id).OrderBy(i => i).ToArray(); + Assert.Equal(3, ids.Distinct().Count()); // all unique + } + + [Fact] + public void BuildEntitiesFromInfo_UnsupportedIdType_IsSkipped() + { + // 0x03xxxxxx is neither GfxObj (0x01) nor Setup (0x02). + var info = new LandBlockInfo + { + Objects = + { + new Stab { Id = 0x01000001u, Frame = new Frame() }, + new Stab { Id = 0x03000002u, Frame = new Frame() }, // skipped + new Stab { Id = 0x02000003u, Frame = new Frame() }, + }, + }; + + var entities = LandblockLoader.BuildEntitiesFromInfo(info); + + Assert.Equal(2, entities.Count); + Assert.DoesNotContain(entities, e => e.SourceGfxObjOrSetupId == 0x03000002u); + } + + [Fact] + public void BuildEntitiesFromInfo_Empty_ReturnsEmpty() + { + var entities = LandblockLoader.BuildEntitiesFromInfo(new LandBlockInfo()); + Assert.Empty(entities); + } +}