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);
+ }
+}