Files
CHOMPStation2/tools/maplint/source/dmm.py
2025-02-21 11:48:45 +04:00

196 lines
6.2 KiB
Python

# I know we already have one in mapmerge, but this one can afford to be significantly simpler to interface with
# by virtue of being read-only.
import re
from dataclasses import dataclass, field
from typing import IO
from .common import Constant, Filename, Null, Typepath
from .error import MapParseError, MaplintError
REGEX_POP_ID = re.compile(r'^"(?P<key>.+)" = \($')
REGEX_POP_CONTENT_HEADER = re.compile(r'^(?P<path>[/\w]+?)(?P<end>[{,)])$')
REGEX_ROW_BEGIN = re.compile(r'^\((?P<x>\d+),(?P<y>\d+),(?P<z>\d+)\) = {"$')
REGEX_VAR_EDIT = re.compile(r'^\t(?P<name>.+?) = (?P<definition>.+?);?$')
@dataclass
class Content:
path: Typepath
filename: str
starting_line: int
var_edits: dict[str, Constant] = field(default_factory = dict)
@dataclass
class DMM:
pops: dict[str, list[Content]] = field(default_factory = dict)
# Z -> X -> Y -> Pop
turfs: list[list[list[str]]] = field(default_factory = list)
def size(self):
return (len(self.turfs[0]), len(self.turfs[0][0]))
def turfs_for_pop(self, key: str):
for z, z_level in enumerate(self.turfs):
for x, x_level in enumerate(z_level):
for y, turf in enumerate(x_level):
if turf == key:
yield (x, y, z)
class DMMParser:
dmm: DMM
line = 0
def __init__(self, reader: IO):
self.dmm = DMM()
self.reader = reader
def parse(self):
if "dmm2tgm" not in self.next_line():
self.raise_error("Map isn't in TGM format. Consider using StrongDMM instead of Dream Maker.\n Please also consider installing the map merge tools, found through Install.bat in the tools/hooks folder.")
try:
while self.parse_pop():
pass
while self.parse_row():
pass
except MapParseError as error:
raise self.raise_error(error)
return self.dmm
def next_line(self):
self.line += 1
try:
return next(self.reader).removesuffix("\n")
except StopIteration:
return None
def parse_pop(self):
line = self.next_line()
if line == "":
return False
pop_match = REGEX_POP_ID.match(line)
if pop_match is None:
self.raise_error("Pops ended too early, expected a newline in between.")
pop_key = pop_match.group("key")
contents = []
while next_line := self.next_line():
next_line = next_line.rstrip()
content_match = REGEX_POP_CONTENT_HEADER.match(next_line)
if content_match is None:
self.raise_error("Pop content didn't lead to a path")
content = Content(Typepath(content_match.group("path")), self.reader.name, self.line)
contents.append(content)
content_end = content_match.group("end")
if content_end == ")":
break
elif content_end == "{":
while (var_edit := self.parse_var_edit()) is not None:
if var_edit[0] == None and var_edit[1] == None:
break
content.var_edits[var_edit[0]] = var_edit[1]
else:
continue # inner loop didn't break
break # inner loop did break indicating a })
elif content_end == ",":
continue
self.dmm.pops[pop_key] = contents
return True
def parse_var_edit(self):
line = self.next_line()
if line == "\t},":
return None
if line == "\t})":
return None, None
var_edit_match = REGEX_VAR_EDIT.match(line)
self.expect(var_edit_match is not None, "Var edits ended too early, expected a newline in between.")
return (var_edit_match.group("name"), self.parse_constant(var_edit_match.group("definition")))
def parse_constant(self, constant):
if (float_constant := self.safe_float(constant)) is not None:
if(constant.isdigit()):
return int(constant)
return float_constant
elif re.match(r'^/[/\w]+$', constant):
return Typepath(constant)
elif re.match(r'^".*"$', constant):
# This should do escaping in the future
return constant[1:-1]
elif re.match(r'^null$', constant):
return Null()
elif re.match(r"^'.*'$", constant):
return Filename(constant[1:-1])
elif (list_match := re.match(r'^list\((?P<contents>.*)\)$', constant)):
return ["NYI: list"]
elif (list_match := re.match(r'^newlist\((?P<contents>.*)\)$', constant)):
return ["NYI: newlist"]
else:
self.raise_error(f"Unknown constant type: {constant}")
def parse_row(self):
line = self.next_line()
if line is None:
return False
if line == "":
# Starting a new z level
return True
row_match = REGEX_ROW_BEGIN.match(line)
self.expect(row_match is not None, "Rows ended too early, expected a newline in between.")
self.expect(row_match.group("y") == "1", "TGM should only be producing individual rows.")
x = int(row_match.group("x")) - 1
z = int(row_match.group("z")) - 1
if len(self.dmm.turfs) <= z:
self.dmm.turfs.append([])
self.expect(len(self.dmm.turfs) == z + 1, "Z coordinate is not sequential")
z_level = self.dmm.turfs[z]
self.expect(len(z_level) == x, "X coordinate is not sequential")
contents = []
while (next_line := self.next_line()) is not None:
next_line = next_line.rstrip()
if next_line == '"}':
break
self.expect(next_line in self.dmm.pops, f"Pop {next_line} is not defined")
contents.append(next_line)
z_level.append(contents)
return True
def safe_float(self, value):
try:
return float(value)
except ValueError:
return None
def expect(self, condition, message):
if not condition:
self.raise_error(message)
def raise_error(self, message):
raise MaplintError(message, self.reader.name, self.line)
def parse_dmm(reader: IO):
return DMMParser(reader).parse()