using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace codelanguage; public class Preprocessor { private readonly string _source; private readonly string _baseDir; private readonly string _fileName; private readonly PreprocessorContext _context; private readonly Dictionary _regexCache = new(); private readonly List<(string File, int Line)> _lineMap = new(); /// Maps a 1-based line in the processed output back to its real source file/line. public IReadOnlyList<(string File, int Line)> LineMap => _lineMap; public (string File, int Line) MapLine(int processedLine) { if (processedLine >= 1 && processedLine <= _lineMap.Count) return _lineMap[processedLine - 1]; if (_lineMap.Count > 0) { var last = _lineMap[^1]; return (last.File, last.Line + Math.Max(0, processedLine - _lineMap.Count)); } return (_fileName, Math.Max(1, processedLine)); } /// Non-fatal `#warn` messages collected during preprocessing (DM: compilation continues). public List Warnings => _context.Warnings; /// Current include policy, restored by #pragma pop. public bool AllowsMultipleIncludes => _context.AllowMultipleIncludes; public Preprocessor(string source, string baseDir, string? fileName = null, HashSet? includeChain = null, Func? fileReader = null) : this(source, baseDir, fileName ?? "main.dm", CreateContext(baseDir, includeChain, fileReader)) { } private Preprocessor(string source, string baseDir, string fileName, PreprocessorContext context) { _source = source; _baseDir = baseDir; _fileName = fileName; _context = context; // The first instance built over a context is the project's main file. // Includes reuse the same context and must not claim __MAIN__. _context.RootFileName ??= fileName; } private static PreprocessorContext CreateContext(string baseDir, HashSet? includeChain, Func? fileReader) { var root = Path.GetFullPath(string.IsNullOrWhiteSpace(baseDir) ? Directory.GetCurrentDirectory() : baseDir); var context = new PreprocessorContext(root, fileReader); if (includeChain != null) { foreach (var path in includeChain) { context.Stack.Add(path); context.Included.Add(path); } } return context; } public string Process() { _lineMap.Clear(); // Join line continuation backslashes (\) var rawLines = _source.Split('\n'); var linesList = new List(); var sourceLines = new List(); for (int ri = 0; ri < rawLines.Length; ri++) { sourceLines.Add(ri + 1); string l = rawLines[ri]; if (l.EndsWith("\r")) l = l.Substring(0, l.Length - 1); if (ri == rawLines.Length - 1 && l.Length == 0) continue; while (l.EndsWith("\\") && ri + 1 < rawLines.Length) { ri++; string nextL = rawLines[ri]; if (nextL.EndsWith("\r")) nextL = nextL.Substring(0, nextL.Length - 1); l = l.Substring(0, l.Length - 1) + " " + nextL.TrimStart(); } linesList.Add(l); } var lines = linesList.ToArray(); var output = new List(); var macros = _context.Macros; // DM defines __MAIN__ only in the main project file. Includes share // this dictionary, so AddIncluded masks the flag while a child runs; // library code therefore cannot see it, matching DM's contract. if (string.Equals(_fileName, _context.RootFileName, StringComparison.OrdinalIgnoreCase)) macros["__MAIN__"] = "1"; var paramMacros = _context.ParamMacros; var conditionalStack = new Stack(); var elseUsed = new Stack(); var branchTaken = new Stack(); var conditionalLines = new Stack(); // Warning policy is part of the pragma state: DM documents #pragma // push/pop as saving and restoring the warning state, not just the // language mode. var pragmaCompatibilityStack = new Stack<(string Version, string Build, string Math, string Syntax, Dictionary Warnings, bool AllowMultiple)>(); void AddOutput(string text, int sourceLine) { var pieces = text.Split('\n'); foreach (var piece in pieces) { output.Add(piece); _lineMap.Add((_fileName, Math.Max(1, sourceLine))); } } void AddIncluded(Preprocessor child, int sourceLine) { bool hadMain = macros.TryGetValue("__MAIN__", out var savedMain); if (hadMain) macros.Remove("__MAIN__"); string childText; try { childText = child.Process(); } finally { if (hadMain) macros["__MAIN__"] = savedMain; } if (childText.Length == 0) return; var childLines = childText.Split('\n'); for (int ci = 0; ci < childLines.Length; ci++) { output.Add(childLines[ci]); if (ci < child.LineMap.Count) { _lineMap.Add(child.LineMap[ci]); } else if (child.LineMap.Count > 0) { var last = child.LineMap[^1]; _lineMap.Add((last.File, last.Line + (ci - child.LineMap.Count + 1))); } else { _lineMap.Add((_fileName, Math.Max(1, sourceLine))); } } } for (int i = 0; i < lines.Length; i++) { string line = lines[i]; string trimmed = line.TrimStart(); if (trimmed.StartsWith("#")) { string directive = trimmed.Substring(1).Trim(); int spaceIdx = directive.IndexOf(' '); string command = spaceIdx >= 0 ? directive.Substring(0, spaceIdx) : directive; string argument = spaceIdx >= 0 ? directive.Substring(spaceIdx + 1).Trim() : ""; switch (command) { case "define": if (!IsConditionallySkipped(conditionalStack)) { string rawArg = argument.Trim(); int openP = rawArg.IndexOf('('); int spaceBeforeOpenP = openP >= 0 ? rawArg.Substring(0, openP).IndexOfAny(new[] { ' ', '\t' }) : -1; if (openP > 0 && spaceBeforeOpenP < 0) { string macroName = rawArg.Substring(0, openP).Trim(); int closeP = rawArg.IndexOf(')', openP); if (closeP > openP) { string paramsStr = rawArg.Substring(openP + 1, closeP - openP - 1); var pList = paramsStr.Split(',') .Select(p => p.Trim()) .Where(p => !string.IsNullOrEmpty(p)) .ToList(); string macroValue = rawArg.Substring(closeP + 1).Trim(); paramMacros[macroName] = (pList, macroValue); } } else { string macroName = argument.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries)[0]; string macroValue = argument.Substring(macroName.Length).Trim(); macros[macroName] = macroValue; } } continue; case "undef": if (!IsConditionallySkipped(conditionalStack)) { string macroName = argument.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries)[0]; macros.Remove(macroName); paramMacros.Remove(macroName); } continue; case "ifdef": conditionalStack.Push(macros.ContainsKey(argument) || paramMacros.ContainsKey(argument)); branchTaken.Push(conditionalStack.Peek()); conditionalLines.Push(sourceLines[i]); elseUsed.Push(false); continue; case "ifndef": conditionalStack.Push(!(macros.ContainsKey(argument) || paramMacros.ContainsKey(argument))); branchTaken.Push(conditionalStack.Peek()); conditionalLines.Push(sourceLines[i]); elseUsed.Push(false); continue; case "if": conditionalStack.Push(EvaluateConditional(argument, macros, paramMacros)); branchTaken.Push(conditionalStack.Peek()); conditionalLines.Push(sourceLines[i]); elseUsed.Push(false); continue; case "elif": if (conditionalStack.Count == 0) throw new ParseError("Unexpected #elif without #if.", sourceLines[i], 1, _fileName); if (elseUsed.Peek()) throw new ParseError("#elif cannot follow #else.", sourceLines[i], 1, _fileName); conditionalStack.Pop(); bool taken = branchTaken.Pop(); bool selected = !taken && !IsConditionallySkipped(conditionalStack) && EvaluateConditional(argument, macros, paramMacros); conditionalStack.Push(selected); branchTaken.Push(taken || selected); continue; case "else": if (conditionalStack.Count == 0) throw new ParseError("Unexpected #else without #if.", sourceLines[i], 1, _fileName); if (elseUsed.Pop()) throw new ParseError("Duplicate #else.", sourceLines[i], 1, _fileName); elseUsed.Push(true); conditionalStack.Pop(); bool hadBranch = branchTaken.Pop(); conditionalStack.Push(!hadBranch && !IsConditionallySkipped(conditionalStack)); branchTaken.Push(true); continue; case "endif": if (conditionalStack.Count == 0) throw new ParseError("Unexpected #endif without #if.", sourceLines[i], 1, _fileName); conditionalStack.Pop(); branchTaken.Pop(); elseUsed.Pop(); conditionalLines.Pop(); continue; case "include": if (!IsConditionallySkipped(conditionalStack)) { string includePath = argument.Trim().Trim('"', '\'', '<', '>'); string fullPath = Path.Combine(_baseDir, includePath); string resolvedFullPath = Path.GetFullPath(fullPath); if (!IsWithinProject(resolvedFullPath)) { throw new ParseError($"Include path '{includePath}' escapes the project directory.", sourceLines[i], 1, _fileName); } if (_context.Stack.Contains(resolvedFullPath)) { throw new ParseError($"Circular include detected: '{includePath}'.", sourceLines[i], 1, _fileName); } if (_context.Included.Contains(resolvedFullPath) && !_context.AllowMultipleIncludes) { continue; } string? virtualText = _context.FileReader?.Invoke(resolvedFullPath); if (virtualText == null && !File.Exists(resolvedFullPath)) { throw new ParseError($"Include file not found: '{includePath}'.", sourceLines[i], 1, _fileName); } _context.Included.Add(resolvedFullPath); _context.Stack.Add(resolvedFullPath); try { if (resolvedFullPath.EndsWith(".cpm", StringComparison.OrdinalIgnoreCase) || resolvedFullPath.EndsWith(".cmap", StringComparison.OrdinalIgnoreCase) || resolvedFullPath.EndsWith(".dmm", StringComparison.OrdinalIgnoreCase)) { var dmm = new CraftMapFile(resolvedFullPath); if (!dmm.Load()) throw new ParseError($"Failed to load map file '{includePath}'.", sourceLines[i], 1, _fileName); AddOutput(ConvertDmmToDm(dmm), sourceLines[i]); } else { string included = virtualText ?? File.ReadAllText(resolvedFullPath); var includedProcessed = new Preprocessor( included, Path.GetDirectoryName(resolvedFullPath) ?? _baseDir, resolvedFullPath, _context); AddIncluded(includedProcessed, sourceLines[i]); } } catch (ParseError) { throw; } catch (Exception ex) { throw new ParseError($"Could not process include '{includePath}': {ex.Message}", sourceLines[i], 1, _fileName); } finally { _context.Stack.Remove(resolvedFullPath); } } continue; case "error": if (!IsConditionallySkipped(conditionalStack)) throw new ParseError($"#error: {argument}", sourceLines[i], 1, _fileName); continue; case "warn": case "warning": if (!IsConditionallySkipped(conditionalStack)) { _context.Warnings.Add($"{Path.GetFileName(_fileName)}:{sourceLines[i]}: #warn: {argument}"); AddOutput($"// [warning] {argument}", sourceLines[i]); } continue; case "pragma": if (!IsConditionallySkipped(conditionalStack)) { var pragmaParts = argument.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (pragmaParts.Length >= 2 && pragmaParts[0].Equals("compatibility", StringComparison.OrdinalIgnoreCase) && int.TryParse(pragmaParts[1], out var compatibilityVersion)) { macros["DM_VERSION"] = compatibilityVersion.ToString(); macros["BYOND_VERSION"] = compatibilityVersion.ToString(); } else if (pragmaParts.Length >= 2 && pragmaParts[0].Equals("math", StringComparison.OrdinalIgnoreCase)) { macros["DM_MATH"] = pragmaParts[1].ToLowerInvariant(); } else if (pragmaParts.Length >= 2 && pragmaParts[0].Equals("syntax", StringComparison.OrdinalIgnoreCase)) { macros["DM_SYNTAX"] = pragmaParts[1].ToUpperInvariant(); } else if (pragmaParts.Length >= 2 && pragmaParts[0].Equals("warn", StringComparison.OrdinalIgnoreCase)) { // DM documents this form as generating a warning // whose *name* appears in compiler output, and // documents #pragma ignore/error as selecting the // policy for that name. Record the policy check // here so those directives actually gate it. string warningName = pragmaParts[1]; string warningText = pragmaParts.Length > 2 ? string.Join(' ', pragmaParts.Skip(2)) : "requested warning"; string policy = macros.GetValueOrDefault( $"DM_WARNING_{warningName.ToUpperInvariant()}", "warn"); if (policy == "ignore") { // Suppressed by #pragma ignore . } else if (policy == "error") { throw new ParseError( $"warning {warningName} treated as an error: {warningText}", sourceLines[i], 1, _fileName); } else { _context.Warnings.Add($"{Path.GetFileName(_fileName)}:{sourceLines[i]}: warning {warningName}: {warningText}"); } } else if (pragmaParts.Length >= 2 && (pragmaParts[0].Equals("ignore", StringComparison.OrdinalIgnoreCase) || pragmaParts[0].Equals("error", StringComparison.OrdinalIgnoreCase))) { macros[$"DM_WARNING_{pragmaParts[1].ToUpperInvariant()}"] = pragmaParts[0].ToLowerInvariant(); } else if (pragmaParts.Length >= 1 && pragmaParts[0].Equals("push", StringComparison.OrdinalIgnoreCase)) { var savedWarnings = macros .Where(pair => pair.Key.StartsWith("DM_WARNING_", StringComparison.Ordinal)) .ToDictionary(pair => pair.Key, pair => pair.Value); pragmaCompatibilityStack.Push((macros.GetValueOrDefault("DM_VERSION", "516"), macros.GetValueOrDefault("BYOND_BUILD", "1688"), macros.GetValueOrDefault("DM_MATH", "accurate"), macros.GetValueOrDefault("DM_SYNTAX", "DM"), savedWarnings, _context.AllowMultipleIncludes)); } else if (pragmaParts.Length >= 1 && pragmaParts[0].Equals("pop", StringComparison.OrdinalIgnoreCase) && pragmaCompatibilityStack.Count > 0) { var previous = pragmaCompatibilityStack.Pop(); macros["DM_VERSION"] = previous.Version; macros["BYOND_VERSION"] = previous.Version; macros["BYOND_BUILD"] = previous.Build; macros["DM_MATH"] = previous.Math; macros["DM_SYNTAX"] = previous.Syntax; // #pragma push/pop saves and restores pragma // state, including the include policy. _context.AllowMultipleIncludes = previous.AllowMultiple; // Restore warning policy set inside the pushed // region instead of leaking it past #pragma pop. foreach (var key in macros.Keys .Where(key => key.StartsWith("DM_WARNING_", StringComparison.Ordinal)) .ToList()) { macros.Remove(key); } foreach (var pair in previous.Warnings) macros[pair.Key] = pair.Value; } else if (pragmaParts.Length >= 1 && pragmaParts[0].Equals("multiple", StringComparison.OrdinalIgnoreCase)) { _context.AllowMultipleIncludes = true; } AddOutput($"// [pragma] {argument}", sourceLines[i]); } continue; default: if (!IsConditionallySkipped(conditionalStack)) AddOutput(SubstituteMacros(line, macros, paramMacros, sourceLines[i]), sourceLines[i]); continue; } } if (!IsConditionallySkipped(conditionalStack)) AddOutput(SubstituteMacros(line, macros, paramMacros, sourceLines[i]), sourceLines[i]); } if (conditionalLines.Count > 0) throw new ParseError("Unclosed conditional directive. Expected #endif.", conditionalLines.Peek(), 1, _fileName); return string.Join("\n", output); } private bool IsWithinProject(string fullPath) { string root = _context.ProjectRoot; if (string.Equals(fullPath, root, StringComparison.OrdinalIgnoreCase)) return true; string prefix = root.EndsWith(Path.DirectorySeparatorChar) ? root : root + Path.DirectorySeparatorChar; return fullPath.StartsWith(prefix, StringComparison.OrdinalIgnoreCase); } internal static bool EvaluateConditionalExpression( string expr, IReadOnlyDictionary macros) { var macroValues = macros.ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal); return EvaluateConditional(expr, macroValues, new Dictionary Params, string Body)>(StringComparer.Ordinal)); } private static bool EvaluateConditional(string expr, Dictionary macros, Dictionary Params, string Body)> paramMacros) { expr = expr.Trim(); if (string.IsNullOrWhiteSpace(expr)) return false; // 1. Handle defined(MACRO) expr = Regex.Replace(expr, @"defined\s*\(\s*([A-Za-z0-9_]+)\s*\)", m => { string name = m.Groups[1].Value; bool defined = macros.ContainsKey(name) || paramMacros.ContainsKey(name); return defined ? "1" : "0"; }); // 2. Replace simple macros in the expression foreach (var macro in macros) { string pattern = $@"\b{Regex.Escape(macro.Key)}\b"; expr = Regex.Replace(expr, pattern, macro.Value); } // Evaluate the boolean operators commonly used in conditional // compilation. Split only at the outermost operator so parenthesized // comparisons and nested expressions remain intact. expr = StripOuterParentheses(expr); int orIndex = FindTopLevelOperator(expr, "||"); if (orIndex >= 0) return EvaluateConditional(expr[..orIndex], macros, paramMacros) || EvaluateConditional(expr[(orIndex + 2)..], macros, paramMacros); int andIndex = FindTopLevelOperator(expr, "&&"); if (andIndex >= 0) return EvaluateConditional(expr[..andIndex], macros, paramMacros) && EvaluateConditional(expr[(andIndex + 2)..], macros, paramMacros); if (expr.StartsWith("!", StringComparison.Ordinal)) return !EvaluateConditional(expr[1..], macros, paramMacros); // If the expression is just a number, evaluate truthiness if (double.TryParse(expr, out double directVal)) { return directVal != 0; } // 3. Simple comparison operations (e.g. A >= B) try { var match = Regex.Match(expr, @"^\s*([0-9.]+)\s*(>=|<=|==|!=|>|<)\s*([0-9.]+)\s*$"); if (match.Success) { double left = double.Parse(match.Groups[1].Value); string op = match.Groups[2].Value; double right = double.Parse(match.Groups[3].Value); return op switch { ">=" => left >= right, "<=" => left <= right, "==" => left == right, "!=" => left != right, ">" => left > right, "<" => left < right, _ => false }; } } catch { } return expr != "0" && !expr.Equals("false", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(expr); } private static string StripOuterParentheses(string value) { value = value.Trim(); while (value.Length >= 2 && value[0] == '(' && value[^1] == ')') { int depth = 0; bool wrapsAll = true; for (int i = 0; i < value.Length; i++) { if (value[i] == '(') depth++; else if (value[i] == ')') depth--; if (depth == 0 && i < value.Length - 1) { wrapsAll = false; break; } } if (!wrapsAll) break; value = value[1..^1].Trim(); } return value; } private static int FindTopLevelOperator(string value, string op) { int depth = 0; for (int i = 0; i <= value.Length - op.Length; i++) { if (value[i] == '(') { depth++; continue; } if (value[i] == ')') { depth--; continue; } if (depth == 0 && value.AsSpan(i, op.Length).SequenceEqual(op)) return i; } return -1; } /// DM predefined macros: __LINE__ expands to the current line number (unquoted), /// __FILE__ expands to the current source file name as a quoted string, /// __DATE__ expands to "Month Day Year", /// __TIME__ expands to "HH:MM:SS", /// __TIMESTAMP__ expands to the combined date-and-time string BYOND emits. private string ExpandBuiltinMacros(string line, int lineNumber) { if (!line.Contains("__LINE__") && !line.Contains("__FILE__") && !line.Contains("__DATE__") && !line.Contains("__TIME__") && !line.Contains("__TIMESTAMP__")) return line; line = Regex.Replace(line, @"\b__LINE__\b", lineNumber.ToString()); line = Regex.Replace(line, @"\b__FILE__\b", $"\"{Path.GetFileName(_fileName)}\""); line = Regex.Replace(line, @"\b__DATE__\b", $"\"{DateTime.Now:MMM dd yyyy}\""); line = Regex.Replace(line, @"\b__TIME__\b", $"\"{DateTime.Now:HH:mm:ss}\""); line = Regex.Replace(line, @"\b__TIMESTAMP__\b", $"\"{DateTime.Now:ddd MMM d HH:mm:ss yyyy}\""); line = Regex.Replace(line, @"\bDM_VERSION\b", "\"516\""); line = Regex.Replace(line, @"\bBYOND_VERSION\b", "\"516\""); line = Regex.Replace(line, @"\bBYOND_BUILD\b", "1688"); return line; } private string SubstituteMacros(string line, Dictionary macros, Dictionary Params, string Body)> paramMacros, int lineNumber = 0) { if (macros.Count == 0 && paramMacros.Count == 0) { if (lineNumber > 0) line = ExpandBuiltinMacros(line, lineNumber); return line; } string trimmed = line.TrimStart(); if (trimmed.StartsWith("#")) return line; var result = new StringBuilder(line.Length); var outsideQuote = new StringBuilder(); char quote = '\0'; bool escaped = false; Regex GetRegex(string key) { if (!_regexCache.TryGetValue(key, out var regex)) { var pattern = $@"(? 0) final = ExpandBuiltinMacros(final, lineNumber); return final; } private string ExpandParamMacros(string segment, Dictionary Params, string Body)> paramMacros) { bool changed = true; int safetyLimit = 100; while (changed && safetyLimit-- > 0) { changed = false; foreach (var kvp in paramMacros) { string name = kvp.Key; var (pList, body) = kvp.Value; int idx = 0; while ((idx = FindMacroCallIndex(segment, name, idx)) >= 0) { int startArgs = idx + name.Length; int endArgs = FindMatchingParenthesis(segment, startArgs); if (endArgs < 0) { idx++; continue; } string argsStr = segment.Substring(startArgs + 1, endArgs - startArgs - 1); var args = ParseMacroArguments(argsStr); if (args.Count != pList.Count) { idx++; continue; } string expandedBody = body; // 1. Stringification: #paramName -> "argVal" (excluding ## token paste) for (int i = 0; i < pList.Count; i++) { string stringifyPattern = $@"(?= 0) { if (idx > 0 && (char.IsLetterOrDigit(text[idx - 1]) || text[idx - 1] == '_')) { idx += name.Length + 1; continue; } return idx; } return -1; } private int FindMatchingParenthesis(string text, int openParenIdx) { int depth = 0; for (int i = openParenIdx; i < text.Length; i++) { if (text[i] == '(') depth++; else if (text[i] == ')') { depth--; if (depth == 0) return i; } } return -1; } private List ParseMacroArguments(string text) { var result = new List(); var current = new StringBuilder(); int parenDepth = 0; int bracketDepth = 0; bool inQuotes = false; char quoteChar = '\0'; for (int i = 0; i < text.Length; i++) { char c = text[i]; if (inQuotes) { current.Append(c); if (c == '\\' && i + 1 < text.Length) { current.Append(text[i + 1]); i++; } else if (c == quoteChar) { inQuotes = false; } continue; } if (c == '"' || c == '\'') { inQuotes = true; quoteChar = c; current.Append(c); continue; } if (c == '(') parenDepth++; else if (c == ')') parenDepth--; else if (c == '[') bracketDepth++; else if (c == ']') bracketDepth--; if (c == ',' && parenDepth == 0 && bracketDepth == 0) { result.Add(current.ToString().Trim()); current.Clear(); } else { current.Append(c); } } result.Add(current.ToString().Trim()); return result; } private bool IsConditionallySkipped(Stack stack) { bool skipped = false; foreach (bool cond in stack) { if (!cond) { skipped = true; break; } } return skipped; } /// /// Converts a loaded DMM map to DM code that creates the map at runtime. /// Generates world size vars and new/TypePath(x,y,z) calls for every tile. /// private static string ConvertDmmToDm(CraftMapFile dmm) { var sb = new StringBuilder(); sb.AppendLine("// Auto-generated from map file"); sb.AppendLine($"world.maxx = {dmm.MaxX};"); sb.AppendLine($"world.maxy = {dmm.MaxY};"); sb.AppendLine($"world.maxz = {dmm.MaxZ};"); sb.AppendLine(); for (int z = 1; z <= dmm.MaxZ; z++) { for (int y = 1; y <= dmm.MaxY; y++) { for (int x = 1; x <= dmm.MaxX; x++) { var key = (x, y, z); if (!dmm.Tiles.TryGetValue(key, out var tileVal) || tileVal == "0" || string.IsNullOrEmpty(tileVal)) continue; var atoms = ParseTileAtomsSimple(tileVal); foreach (var atom in atoms) { string variable = $"__craft_map_atom_{x}_{y}_{z}_{atoms.IndexOf(atom)}"; if (atom.Vars.Count == 0) sb.AppendLine($"new {atom.TypePath}({x}, {y}, {z})"); else { sb.AppendLine($"var/{variable} = new {atom.TypePath}({x}, {y}, {z})"); foreach (var kv in atom.Vars) { string value = kv.Value.Replace("\\", "\\\\").Replace("\"", "\\\""); sb.AppendLine($"{variable}.{kv.Key} = \"{value}\""); } } } } } } return sb.ToString(); } private static List<(string TypePath, Dictionary Vars)> ParseTileAtomsSimple(string tileId) { var result = new List<(string TypePath, Dictionary Vars)>(); if (tileId == "0" || string.IsNullOrEmpty(tileId)) return result; int depth = 0; var sb = new StringBuilder(); foreach (char c in tileId) { if (c == '{') depth++; else if (c == '}') depth--; if (c == ',' && depth <= 0) { var atom = sb.ToString().Trim(); if (atom.Length > 0) result.Add(ParseSingleAtom(atom)); sb.Clear(); } else { sb.Append(c); } } var last = sb.ToString().Trim(); if (last.Length > 0) result.Add(ParseSingleAtom(last)); return result; } private static (string TypePath, Dictionary Vars) ParseSingleAtom(string atomDef) { var vars = new Dictionary(); if (string.IsNullOrEmpty(atomDef)) return ("", vars); int braceIdx = atomDef.IndexOf('{'); string typePath = (braceIdx >= 0 ? atomDef.Substring(0, braceIdx) : atomDef).Trim(); if (braceIdx >= 0) { int closeIdx = atomDef.LastIndexOf('}'); if (closeIdx > braceIdx) { string body = atomDef.Substring(braceIdx + 1, closeIdx - braceIdx - 1); foreach (var stmt in body.Split(';')) { int eq = stmt.IndexOf('='); if (eq <= 0) continue; string key = stmt.Substring(0, eq).Trim(); string val = stmt.Substring(eq + 1).Trim().Trim('"'); if (!string.IsNullOrEmpty(key)) vars[key] = val; } } } return (typePath, vars); } internal sealed class PreprocessorContext { public Dictionary Macros { get; } = new(StringComparer.Ordinal); public Dictionary Params, string Body)> ParamMacros { get; } = new(StringComparer.Ordinal); public HashSet Included { get; } = new(StringComparer.OrdinalIgnoreCase); public HashSet Stack { get; } = new(StringComparer.OrdinalIgnoreCase); public List Warnings { get; } = new(); public string ProjectRoot { get; } public Func? FileReader { get; } public bool AllowMultipleIncludes { get; set; } /// Path of the project's main file; only that file defines __MAIN__. public string? RootFileName { get; set; } public PreprocessorContext(string projectRoot, Func? fileReader) { ProjectRoot = projectRoot; FileReader = fileReader; // Current official stable baseline. These are predefined by BYOND and // are required by compatibility guards in modern DM source. Macros["DM_VERSION"] = "516"; Macros["BYOND_VERSION"] = "516"; Macros["BYOND_BUILD"] = "1688"; Macros["DM_MATH"] = "accurate"; Macros["DM_SYNTAX"] = "DM"; } } }