using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace codelanguage; public class CraftMapFile : CraftFile { public int MaxX { get; set; } public int MaxY { get; set; } public int MaxZ { get; set; } public Dictionary<(int x, int y, int z), string> Tiles { get; set; } = new(); public Dictionary TileDefinitions { get; set; } = new(); public CraftMapFile(string path) : base(path) { } public override bool Load() { try { ParseDmm(File.ReadAllBytes(Path).Length == 0 ? string.Empty : File.ReadAllText(Path)); LastModified = File.GetLastWriteTime(Path); return true; } catch { return false; } } public override bool Save() { try { StringBuilder sb = new(); var uniqueTiles = Tiles.Values .Where(v => !string.IsNullOrWhiteSpace(v)) .Distinct(StringComparer.Ordinal) .ToList(); var tileToKey = new Dictionary(StringComparer.Ordinal); int existingKeyLength = TileDefinitions.Keys .Where(key => !string.IsNullOrWhiteSpace(key)) .OrderByDescending(key => key.Length) .FirstOrDefault()?.Length ?? 0; TileDefinitions.Clear(); bool needsFallbackEmptyKey = uniqueTiles.Count > 0 && !uniqueTiles.Any(t => t.Contains("/turf/space", StringComparison.OrdinalIgnoreCase) || t.Contains("/turf/default", StringComparison.OrdinalIgnoreCase)); int totalKeyCount = Math.Max(1, uniqueTiles.Count + (needsFallbackEmptyKey ? 1 : 0)); int keyLength = existingKeyLength > 0 ? Math.Max(existingKeyLength, KeyLengthFor(totalKeyCount)) : KeyLengthFor(totalKeyCount); if (keyLength < 1) keyLength = 1; for (int i = 0; i < uniqueTiles.Count; i++) { string key = GenerateKey(i, keyLength); tileToKey[uniqueTiles[i]] = key; TileDefinitions[key] = uniqueTiles[i]; } string emptyKey; var matchingSpace = uniqueTiles.FirstOrDefault(t => t.Contains("/turf/space", StringComparison.OrdinalIgnoreCase) || t.Contains("/turf/default", StringComparison.OrdinalIgnoreCase)); if (matchingSpace != null) { emptyKey = tileToKey[matchingSpace]; } else { string key = GenerateKey(Math.Max(0, uniqueTiles.Count), keyLength); tileToKey["0"] = key; tileToKey[string.Empty] = key; TileDefinitions[key] = "/turf/space"; emptyKey = key; } tileToKey["0"] = emptyKey; tileToKey[string.Empty] = emptyKey; foreach (var definition in TileDefinitions.OrderBy(def => def.Key, StringComparer.Ordinal)) { sb.AppendLine($"\"{definition.Key}\" = ({definition.Value})"); } if (TileDefinitions.Count > 0) sb.AppendLine(); for (int z = 1; z <= Math.Max(1, MaxZ); z++) { sb.AppendLine($"(1,1,{z}) = {{\""); for (int y = Math.Max(1, MaxY); y >= 1; y--) { var row = new StringBuilder(); for (int x = 1; x <= Math.Max(1, MaxX); x++) { var key = (x, y, z); var value = Tiles.TryGetValue(key, out var tile) && !string.IsNullOrWhiteSpace(tile) ? tile : "0"; row.Append(tileToKey.TryGetValue(value, out var tileKey) ? tileKey : emptyKey); } sb.AppendLine(row.ToString()); } sb.AppendLine("\"}"); } File.WriteAllText(Path, sb.ToString(), new UTF8Encoding(encoderShouldEmitUTF8Identifier: true)); LastModified = File.GetLastWriteTime(Path); return true; } catch { return false; } } private const string KeyAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; public static int KeyLengthFor(int distinctTileCount) { int count = Math.Max(0, distinctTileCount); if (count == 0) return 1; long capacity = KeyAlphabet.Length; int width = 1; while (count > capacity) { width++; capacity *= KeyAlphabet.Length; } return width; } public static string GenerateKey(int index, int keyLength) { if (index < 0) throw new ArgumentOutOfRangeException(nameof(index)); if (keyLength < 1 || keyLength > 6) throw new ArgumentOutOfRangeException(nameof(keyLength)); long capacity = (long)Math.Pow(KeyAlphabet.Length, keyLength); if (index >= capacity) throw new ArgumentOutOfRangeException(nameof(index)); int alphabet = KeyAlphabet.Length; var chars = new char[keyLength]; int value = index; for (int pos = keyLength - 1; pos >= 0; pos--) { int remainder = value % alphabet; if (remainder < 0) remainder += alphabet; chars[pos] = KeyAlphabet[remainder]; value /= alphabet; } return new string(chars); } private void ParseDmm(string content) { Tiles.Clear(); TileDefinitions.Clear(); MaxX = 0; MaxY = 0; MaxZ = 0; string normalized = content.Replace("\r\n", "\n").Replace('\r', '\n'); var lines = normalized.Split('\n'); foreach (string rawLine in lines) { string line = rawLine.Trim(); if (string.IsNullOrEmpty(line) || line.StartsWith("//", StringComparison.Ordinal)) continue; if (TryLoadDefinition(line)) continue; try { if (TryLoadCoordinateTile(line)) continue; } catch { // Valid coordinate-record parsing remains tolerant so tool metadata can // be carried in the same DMM stream. } } int index = 0; while (index < lines.Length) { string line = lines[index].Trim(); if (string.IsNullOrEmpty(line) || line.StartsWith("//", StringComparison.Ordinal)) { index++; continue; } if (!line.StartsWith("(", StringComparison.Ordinal)) { index++; continue; } Match header = Regex.Match(line, @"^\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*=\s*\{"); if (!header.Success) { index++; continue; } int originX = ParsePositive(header.Groups[1].Value); int originY = ParsePositive(header.Groups[2].Value); int originZ = ParsePositive(header.Groups[3].Value); int braceIndex = line.IndexOf('{'); var blockRows = new List(); int currentIndex = index; // A block may place its single row on the header line using BYOND's // `{ "..." }` form. Find the opening quote after the brace and its // matching closing row quote, rather than slicing off the brace. int rowOpenQuote = line.IndexOf('"', braceIndex + 1); int rowCloseQuote = rowOpenQuote >= 0 ? line.IndexOf("\"}", rowOpenQuote) : -1; if (rowOpenQuote >= 0 && rowCloseQuote >= rowOpenQuote) { int rowEndQuote = rowCloseQuote + 1; string rowText = line.Substring(rowOpenQuote, rowEndQuote - rowOpenQuote); AddBlockRowText(rowText, blockRows); currentIndex = index + 1; } else if (rowOpenQuote < 0 && line.IndexOf("}", braceIndex + 1) < 0) { // A header with no opening fence is treated as an empty block so a // malformed fence cannot swallow unrelated map text. currentIndex = index + 1; } else { currentIndex = index + 1; bool closed = false; while (currentIndex < lines.Length) { string currentLine = lines[currentIndex].Trim(); if (string.IsNullOrEmpty(currentLine)) { currentIndex++; continue; } int currentClose = currentLine.IndexOf("\"}"); if (currentClose >= 0) { string prefix = currentLine.Substring(0, currentClose).Trim(); // Avoid collecting the closing brace itself as a tile row. if (prefix != "\"" && !string.IsNullOrEmpty(prefix)) AddBlockRowText(prefix, blockRows); closed = true; currentIndex++; break; } if (currentLine == "\"}" || currentLine == "}") { closed = true; currentIndex++; break; } if (!string.IsNullOrEmpty(currentLine)) AddBlockRowText(currentLine, blockRows); currentIndex++; } if (!closed) throw new FormatException("Unterminated DMM map block."); } var blockWidth = TileDefinitions.Keys .Where(key => !string.IsNullOrWhiteSpace(key)) .Select(key => key.Length) .DefaultIfEmpty(1) .Max(); if (blockWidth < 1) blockWidth = 1; ParseMapBlock(originX, originY, originZ, blockWidth, blockRows); index = currentIndex; } } private bool TryLoadDefinition(string line) { int equals = line.IndexOf('='); if (equals <= 0) return false; string leftRaw = line.Substring(0, equals).Trim(); string left = StripQuotes(leftRaw); if (string.IsNullOrWhiteSpace(left)) return false; if (left.StartsWith("(", StringComparison.Ordinal)) return false; string right = line.Substring(equals + 1).Trim(); if (right.StartsWith("(") && right.EndsWith(")") && right.Length >= 2) right = right.Substring(1, right.Length - 2).Trim(); if (right.StartsWith("\"") && right.EndsWith("\"") && right.Length >= 2) right = right.Substring(1, right.Length - 2); if (string.IsNullOrWhiteSpace(right)) return false; TileDefinitions[left] = right; return true; } private bool TryLoadCoordinateTile(string line) { int equals = line.IndexOf('='); if (equals <= 0) return false; string left = line.Substring(0, equals).Trim(); var parts = left.Split(','); if (parts.Length != 3) return false; if (!int.TryParse(parts[0].Trim(), out var x) || !int.TryParse(parts[1].Trim(), out var y) || !int.TryParse(parts[2].Trim(), out var z)) { return false; } if (x < 1 || y < 1 || z < 1) throw new ArgumentOutOfRangeException(nameof(x), "DMM coordinates must be positive."); string right = line.Substring(equals + 1).Trim().Trim('"'); if (right.StartsWith("(") && right.EndsWith(")") && right.Length >= 2) right = right.Substring(1, right.Length - 2).Trim(); if (right.StartsWith("\"") && right.EndsWith("\"") && right.Length >= 2) right = right.Substring(1, right.Length - 2); Tiles[(x, y, z)] = right; UpdateBounds(x, y, z); return true; } private static int ParsePositive(string value) { if (!int.TryParse(value, out int result) || result < 1) throw new ArgumentOutOfRangeException(nameof(value), "DMM coordinate must be positive."); return result; } private static void AddBlockRowText(string text, List rows, bool stripOuterQuote = false) { string row = text; if (stripOuterQuote && row.Length >= 2 && row.StartsWith("\"") && row.EndsWith("\"")) row = row.Substring(1, row.Length - 2); // A BYOND row is quoted. Strip the outer row quote only when the caller did // not already request it above. if (!stripOuterQuote && row.Length >= 2 && row.StartsWith("\"") && row.EndsWith("\"")) row = row.Substring(1, row.Length - 2); if (!string.IsNullOrEmpty(row)) rows.Add(row); } private void ParseMapBlock(int originX, int originY, int originZ, int keyWidth, List blockRows) { if (originX < 1 || originY < 1 || originZ < 1 || keyWidth < 1) throw new ArgumentOutOfRangeException(); for (int rowIndex = 0; rowIndex < blockRows.Count; rowIndex++) { string rowText = blockRows[rowIndex]; int height = blockRows.Count; int tileY = originY + (height - 1 - rowIndex); int columnIndex = 0; int tileX = originX; if (rowText.Length % keyWidth != 0) throw new FormatException("DMM map row width is not aligned with its tile key width."); for (int charIndex = 0; charIndex + keyWidth <= rowText.Length; charIndex += keyWidth) { string definitionKey = rowText.Substring(charIndex, keyWidth); string tileValue = TileDefinitions.TryGetValue(definitionKey, out var definition) ? definition : "0"; if (!string.IsNullOrWhiteSpace(tileValue) && tileValue != "0") Tiles[(tileX, tileY, originZ)] = tileValue; else Tiles.Remove((tileX, tileY, originZ)); UpdateBounds(tileX, tileY, originZ); columnIndex++; tileX = originX + columnIndex; } } } private void UpdateBounds(int x, int y, int z) { MaxX = Math.Max(MaxX, x); MaxY = Math.Max(MaxY, y); MaxZ = Math.Max(MaxZ, z); } private static string StripQuotes(string value) { if (value.Length >= 2 && value.StartsWith("\"") && value.EndsWith("\"")) return value.Substring(1, value.Length - 2); return value; } }