using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Types; using AcDream.Core.Physics; 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, landblockId); 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, uint landblockId = 0) { var result = new List(info.Objects.Count + info.Buildings.Count); // When landblockId is non-zero, namespace stab Ids globally: // 0xC0XXYY00 + n, where XX = lbX byte, YY = lbY byte // distinct from the 0x8XXYYIII scenery and 0x4XXYYIII interior // namespaces in GameWindow.cs. The 0xC0 top byte distinguishes stabs. // // Pre-Tier-1 callers (existing tests) pass landblockId=0 and get the // legacy starting-from-1 monotonic Ids — compatible with their assertions // which check uniqueness within a single landblock. // // The low byte reserves 0 for the namespace base and 1..255 for // entities. Never wrap into the Y byte: a collision here would corrupt // every renderer/physics/effect table keyed by WorldEntity.Id. uint stabIdBase = landblockId == 0 ? 0u : 0xC0000000u | ((landblockId >> 24) & 0xFFu) << 16 | ((landblockId >> 16) & 0xFFu) << 8; uint nextId = stabIdBase == 0 ? 1u : stabIdBase + 1u; uint AllocateId() { if (stabIdBase != 0 && nextId > stabIdBase + 0xFFu) throw new InvalidDataException( $"Landblock 0x{landblockId:X8} exceeds the 255-entry static stab id namespace."); return nextId++; } foreach (var stab in info.Objects) { if (!IsSupported(stab.Id)) continue; var stabEntity = new WorldEntity { Id = AllocateId(), SourceGfxObjOrSetupId = stab.Id, Position = stab.Frame.Origin, Rotation = stab.Frame.Orientation, MeshRefs = Array.Empty(), EffectCellId = OutdoorCellId(landblockId, stab.Frame.Origin), }; stabEntity.RefreshAabb(); // A.5 T18: populate cached AABB at construction result.Add(stabEntity); } foreach (var building in info.Buildings) { if (!IsSupported(building.ModelId)) continue; var buildingEntity = new WorldEntity { Id = AllocateId(), SourceGfxObjOrSetupId = building.ModelId, Position = building.Frame.Origin, Rotation = building.Frame.Orientation, MeshRefs = Array.Empty(), EffectCellId = OutdoorCellId(landblockId, building.Frame.Origin), IsBuildingShell = true, // Phase A8: tag at source array boundary BuildingShellAnchorCellId = FirstBuildingAnchorCellId(building, landblockId), }; buildingEntity.RefreshAabb(); // A.5 T18: populate cached AABB at construction result.Add(buildingEntity); } return result; } private static bool IsSupported(uint id) { var type = id & TypeMask; return type == GfxObjMask || type == SetupMask; } private static uint? OutdoorCellId(uint landblockId, System.Numerics.Vector3 localPosition) { if (landblockId == 0) return null; return TerrainSurface.ComputeOutdoorCellId( landblockId, localPosition.X, localPosition.Y); } private static uint? FirstBuildingAnchorCellId(BuildingInfo building, uint landblockId) { if (landblockId == 0) return null; uint lbPrefix = landblockId & 0xFFFF0000u; foreach (var portal in building.Portals) { if (portal.OtherCellId == 0xFFFF) continue; return lbPrefix | (uint)portal.OtherCellId; } return null; } }