using System.Numerics; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using DatReaderWriter.Options; using SysEnv = System.Environment; string datDir = SysEnv.GetEnvironmentVariable("ACDREAM_DAT_DIR") ?? Path.Combine(SysEnv.GetFolderPath(SysEnv.SpecialFolder.UserProfile), "Documents", "Asheron's Call"); if (!Directory.Exists(datDir)) { Console.Error.WriteLine($"DAT directory not found: {datDir}"); return 2; } using var dats = new DatCollection(datDir, DatAccessType.Read); if (args.Length > 0 && string.Equals(args[0], "buildings", StringComparison.OrdinalIgnoreCase)) { uint landblockId = args.Length > 1 ? ParseHex(args[1]) : 0xA9B40000u; int radius = args.Length > 2 ? int.Parse(args[2], System.Globalization.CultureInfo.InvariantCulture) : 0; DumpBuildings(dats, landblockId, radius); } else if (args.Length > 0 && string.Equals(args[0], "portals", StringComparison.OrdinalIgnoreCase)) { var ids = args.Length == 1 ? new uint[] { 0xA9B40171u } : args.Skip(1).Select(ParseHex).ToArray(); foreach (var envCellId in ids) DumpCellPortals(dats, envCellId); } else { var ids = args.Length == 0 ? [0xA9B4013Fu] : args.Select(ParseHex).ToArray(); foreach (var envCellId in ids) { DumpCell(dats, envCellId); } } return 0; static uint ParseHex(string text) { text = text.Trim(); if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) return Convert.ToUInt32(text[2..], 16); return Convert.ToUInt32(text, 16); } static void DumpCell(DatCollection dats, uint envCellId) { Console.WriteLine($"=== EnvCell 0x{envCellId:X8} ==="); var envCell = dats.Get(envCellId); if (envCell is null) { Console.WriteLine("missing EnvCell"); return; } var envId = 0x0D000000u | envCell.EnvironmentId; var environment = dats.Get(envId); if (environment is null) { Console.WriteLine($"missing Environment 0x{envId:X8}"); return; } if (!environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct)) { Console.WriteLine($"missing CellStruct {envCell.CellStructure}"); return; } Console.WriteLine( $"environment=0x{envId:X8} cellStruct={envCell.CellStructure} " + $"surfaces={envCell.Surfaces.Count} verts={cellStruct.VertexArray.Vertices.Count} polys={cellStruct.Polygons.Count}"); int posSides = 0; int negSides = 0; int skipped = 0; int likelyFloor = 0; int likelyCeiling = 0; foreach (var (polyId, poly) in cellStruct.Polygons.OrderBy(p => p.Key)) { if (poly.VertexIds.Count < 3) continue; bool emitPos = !poly.Stippling.HasFlag(StipplingType.NoPos) && poly.PosSurface >= 0 && poly.PosSurface < envCell.Surfaces.Count; bool emitNeg = (poly.Stippling.HasFlag(StipplingType.Negative) || poly.Stippling.HasFlag(StipplingType.Both) || (!poly.Stippling.HasFlag(StipplingType.NoNeg) && poly.SidesType == CullMode.Clockwise)) && poly.NegSurface >= 0 && poly.NegSurface < envCell.Surfaces.Count; if (emitPos) posSides++; if (emitNeg) negSides++; if (!emitPos && !emitNeg) skipped++; var normal = ComputeNormal(cellStruct, poly); var uvRange = UvRange(cellStruct, poly); string planeHint = Math.Abs(normal.Z) > 0.9f ? normal.Z > 0 ? "floor/up" : "ceiling/down" : Math.Abs(normal.Z) > 0.15f ? "slope" : "wall"; if (normal.Z > 0.9f) likelyFloor++; if (normal.Z < -0.9f) likelyCeiling++; var posSurf = SurfaceText(envCell, poly.PosSurface); var negSurf = SurfaceText(envCell, poly.NegSurface); Console.WriteLine( $"poly=0x{polyId:X4} pts={poly.VertexIds.Count} n=({normal.X:F3},{normal.Y:F3},{normal.Z:F3}) {planeHint,-12} " + $"stip={poly.Stippling} sides={poly.SidesType} pos={poly.PosSurface}->{posSurf} neg={poly.NegSurface}->{negSurf} " + $"emitPos={emitPos} emitNeg={emitNeg} posUv={poly.PosUVIndices?.Count ?? 0} negUv={poly.NegUVIndices?.Count ?? 0} " + $"uv=({uvRange.Min.X:F3},{uvRange.Min.Y:F3})..({uvRange.Max.X:F3},{uvRange.Max.Y:F3})"); } Console.WriteLine( $"summary: posSides={posSides} negSides={negSides} skipped={skipped} " + $"likelyFloor={likelyFloor} likelyCeiling={likelyCeiling}"); Console.WriteLine(); } static void DumpCellPortals(DatCollection dats, uint envCellId) { Console.WriteLine($"=== EnvCell 0x{envCellId:X8} portals ==="); var envCell = dats.Get(envCellId); if (envCell is null) { Console.WriteLine("missing EnvCell"); return; } uint lbPrefix = envCellId & 0xFFFF0000u; int exitCount = 0; int interiorCount = 0; for (int i = 0; i < envCell.CellPortals.Count; i++) { var portal = envCell.CellPortals[i]; bool isExit = portal.OtherCellId == 0xFFFF; if (isExit) exitCount++; else interiorCount++; string dest = isExit ? "EXIT(outdoor)" : $"0x{(lbPrefix | (uint)portal.OtherCellId):X8}"; Console.WriteLine( $" portal[{i}] other=0x{portal.OtherCellId:X4} -> {dest} " + $"flags={portal.Flags} polyId={portal.PolygonId}"); } Console.WriteLine( $"summary: cell=0x{envCellId:X8} portals={envCell.CellPortals.Count} " + $"exits(0xFFFF)={exitCount} interior={interiorCount} " + $"numCellPortals={envCell.CellPortals.Count} seenOutside={(envCell.Flags.HasFlag(EnvCellFlags.SeenOutside) ? "Y" : "n")}"); Console.WriteLine(); } static void DumpBuildings(DatCollection dats, uint centerLandblockId, int radius) { int centerX = (int)((centerLandblockId >> 24) & 0xFFu); int centerY = (int)((centerLandblockId >> 16) & 0xFFu); int totalRegistryBuildings = 0; int totalShellEntities = 0; for (int dy = -radius; dy <= radius; dy++) for (int dx = -radius; dx <= radius; dx++) { int x = centerX + dx; int y = centerY + dy; if (x < 0 || x > 255 || y < 0 || y > 255) continue; uint landblockId = ((uint)x << 24) | ((uint)y << 16); var info = dats.Get(landblockId | 0xFFFEu); if (info is null) continue; var (registryBuildings, shellEntities) = DumpLandblockBuildings(info, landblockId); totalRegistryBuildings += registryBuildings; totalShellEntities += shellEntities; } Console.WriteLine( $"radius-summary center=0x{(centerLandblockId & 0xFFFF0000u):X8} radius={radius} " + $"registryBuildings={totalRegistryBuildings} shellEntities={totalShellEntities}"); } static (int RegistryBuildings, int ShellEntities) DumpLandblockBuildings(LandBlockInfo info, uint landblockId) { uint lbPrefix = landblockId & 0xFFFF0000u; uint stabIdBase = 0xC0000000u | (((landblockId >> 24) & 0xFFu) << 16) | (((landblockId >> 16) & 0xFFu) << 8); uint nextEntityId = stabIdBase + 1u; int supportedObjects = 0; foreach (var obj in info.Objects) { if (!IsSupported(obj.Id)) continue; supportedObjects++; nextEntityId++; } Console.WriteLine( $"=== Landblock 0x{lbPrefix:X8} info=0x{(lbPrefix | 0xFFFEu):X8} " + $"objects={info.Objects.Count} supportedObjects={supportedObjects} buildings={info.Buildings.Count} ==="); int registryBuildingId = 1; int shellEntities = 0; foreach (var (building, zeroBased) in info.Buildings.Select((b, i) => (b, i))) { if (!IsSupported(building.ModelId)) continue; uint shellEntityId = nextEntityId++; shellEntities++; var portalCells = building.Portals .Where(p => p.OtherCellId != 0xFFFF) .Select(p => lbPrefix | (uint)p.OtherCellId) .Distinct() .OrderBy(id => id) .ToArray(); string registryText = portalCells.Length == 0 ? "none" : $"0x{registryBuildingId++:X}"; string portalText = portalCells.Length == 0 ? "[]" : "[" + string.Join(",", portalCells.Select(id => $"0x{id:X8}")) + "]"; Console.WriteLine( $"buildingOrdinal={zeroBased + 1} registryId={registryText} shellEntity=0x{shellEntityId:X8} " + $"model=0x{building.ModelId:X8} pos=({building.Frame.Origin.X:F2},{building.Frame.Origin.Y:F2},{building.Frame.Origin.Z:F2}) " + $"portalCells={portalText}"); } Console.WriteLine(); return (registryBuildingId - 1, shellEntities); } static bool IsSupported(uint id) { uint type = id & 0xFF000000u; return type == 0x01000000u || type == 0x02000000u; } static string SurfaceText(EnvCell envCell, short index) { if (index < 0) return "none"; if (index >= envCell.Surfaces.Count) return "out-of-range"; return $"0x{(0x08000000u | envCell.Surfaces[index]):X8}"; } static Vector3 ComputeNormal(DatReaderWriter.Types.CellStruct cellStruct, DatReaderWriter.Types.Polygon poly) { if (poly.VertexIds.Count < 3) return Vector3.Zero; if (!TryGetOrigin(cellStruct, (ushort)poly.VertexIds[0], out var a) || !TryGetOrigin(cellStruct, (ushort)poly.VertexIds[1], out var b) || !TryGetOrigin(cellStruct, (ushort)poly.VertexIds[2], out var c)) { return Vector3.Zero; } var n = Vector3.Cross(b - a, c - a); return n.LengthSquared() > 0f ? Vector3.Normalize(n) : Vector3.Zero; } static bool TryGetOrigin(DatReaderWriter.Types.CellStruct cellStruct, ushort id, out Vector3 origin) { if (cellStruct.VertexArray.Vertices.TryGetValue(id, out var vertex)) { origin = vertex.Origin; return true; } origin = Vector3.Zero; return false; } static (Vector2 Min, Vector2 Max) UvRange(DatReaderWriter.Types.CellStruct cellStruct, DatReaderWriter.Types.Polygon poly) { var min = new Vector2(float.MaxValue, float.MaxValue); var max = new Vector2(float.MinValue, float.MinValue); bool any = false; for (int i = 0; i < poly.VertexIds.Count; i++) { if (!cellStruct.VertexArray.Vertices.TryGetValue((ushort)poly.VertexIds[i], out var vertex)) continue; ushort uvIdx = 0; if (poly.PosUVIndices is not null && i < poly.PosUVIndices.Count) uvIdx = poly.PosUVIndices[i]; if (uvIdx >= vertex.UVs.Count) uvIdx = 0; if (vertex.UVs.Count == 0) continue; var uv = new Vector2(vertex.UVs[uvIdx].U, vertex.UVs[uvIdx].V); min = Vector2.Min(min, uv); max = Vector2.Max(max, uv); any = true; } return any ? (min, max) : (Vector2.Zero, Vector2.Zero); }