mirror of
https://github.com/VOREStation/VOREStation.git
synced 2026-05-19 05:09:49 +01:00
9f124e5b14
* Initial * Remove corrupt dmis * Fixup maps in TGM format4e5a32721f: maps/_templates_and_guidance/Public Event Templates/Maze_Reward_-_Copy.dmm4e5a32721f: maps/_templates_and_guidance/Templates/shelter_Medical.dmm4e5a32721f: maps/expedition_vr/aerostat/aerostat.dmm4e5a32721f: maps/expedition_vr/aerostat/aerostat_science_outpost.dmm4e5a32721f: maps/expedition_vr/beach/submaps/deadBeacon.dmm4e5a32721f: maps/expedition_vr/wild/tether_wild-crash-alt.dmm4e5a32721f: maps/expedition_vr/wild/tether_wild-crash.dmm4e5a32721f: maps/expedition_vr/wild/tether_wild-surface.dmm4e5a32721f: maps/expedition_vr/wild/tether_wild-temple.dmm4e5a32721f: maps/gateway_vr/lucky_7.dmm4e5a32721f: maps/gateway_vr/snow_outpost.dmm4e5a32721f: maps/overmap/_map.dmm4e5a32721f: maps/overmap/bearcat/bearcat.dmm4e5a32721f: maps/overmap/example_sector1.dmm4e5a32721f: maps/overmap/example_sector2.dmm4e5a32721f: maps/redgate/falls/falls.dmm4e5a32721f: maps/submaps/pois_vr/aerostat/CaveS.dmm4e5a32721f: maps/submaps/pois_vr/aerostat/DeadSettlers1.dmm4e5a32721f: maps/submaps/pois_vr/aerostat/DeadSettlers2.dmm4e5a32721f: maps/submaps/pois_vr/aerostat/DoomP.dmm4e5a32721f: maps/submaps/pois_vr/aerostat/Lab1.dmm4e5a32721f: maps/submaps/pois_vr/aerostat/Rockybase.dmm4e5a32721f: maps/submaps/pois_vr/debris_field/debris14.dmm4e5a32721f: maps/submaps/pois_vr/debris_field/derelict.dmm4e5a32721f: maps/submaps/pois_vr/debris_field/new_escapepod_xeno.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/BlastMine1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/CaveTrench.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/Cavelake.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/Cliff1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/CrashedMedShuttle1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/Geyser1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/Geyser2.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/Geyser3.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/Mineshaft1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/Scave1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/SupplyDrop1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/crashed_ufo.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/crashed_ufo_frigate.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/crashedcontainmentshuttle.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/crystal1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/crystal2.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/crystal3.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/deadBeacon.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/deadly_rabbit_vr.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/deadspy.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/digsite.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/excavation1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/lava_trench.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/prepper1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/ritual.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/spatial_anomaly.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/speakeasy_vr.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/vault1.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/vault2.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/vault3.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/vault4.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/vault5.dmm4e5a32721f: maps/submaps/surface_submaps/mountains/vault6.dmm4e5a32721f: maps/submaps/surface_submaps/plains/Boathouse.dmm4e5a32721f: maps/submaps/surface_submaps/plains/BuriedTreasure.dmm4e5a32721f: maps/submaps/surface_submaps/plains/BuriedTreasure2.dmm4e5a32721f: maps/submaps/surface_submaps/plains/BuriedTreasure3.dmm4e5a32721f: maps/submaps/surface_submaps/plains/Oldhouse.dmm4e5a32721f: maps/submaps/surface_submaps/plains/PooledR.dmm4e5a32721f: maps/submaps/surface_submaps/plains/Rocky5.dmm4e5a32721f: maps/submaps/surface_submaps/plains/Shakden.dmm4e5a32721f: maps/submaps/surface_submaps/plains/Thiefc.dmm4e5a32721f: maps/submaps/surface_submaps/plains/beacons.dmm4e5a32721f: maps/submaps/surface_submaps/plains/chemspill1.dmm4e5a32721f: maps/submaps/surface_submaps/plains/farm1.dmm4e5a32721f: maps/submaps/surface_submaps/plains/house1.dmm4e5a32721f: maps/submaps/surface_submaps/plains/lonehome.dmm4e5a32721f: maps/submaps/surface_submaps/plains/smol2.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Blackshuttledown.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Blueshuttledown.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Boombase.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/CaveS.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Chapel.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Cragzone1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/DJOutpost1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/DJOutpost2.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/DJOutpost3.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/DJOutpost4.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/DecoupledEngine.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/DoomP.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Drugden.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Epod3.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Epod4.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Flake.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/FrostflyNest.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/MCamp1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/MHR.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Manor1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Mudpit.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Rocky1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Rocky3.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Rocky4.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Rockybase.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Shack1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Shelter.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Smol1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/Snowrock1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/borglab.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/butchershack.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/chasm.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/chemspill2.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/deathden.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/derelictengine.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/frostoasis.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/kururakden.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/spider1.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/wolfden.dmm4e5a32721f: maps/submaps/surface_submaps/wilderness/xenohive.dmm4e5a32721f: maps/tether/tether-02-surface2.dmm4e5a32721f: maps/virgo_minitest/virgo_minitest-sector-2.dmm Automatically commited by: tools\mapmerge2\fixup.py * Remove unnecessary whitespace edits from mapmerger * Cable dirs update path * Fix area var edits * Put the area over there * Ignore archive maps folder * Forgot to port multivar support too * A few changes I forgot about for hook install * restore multivar support that chomp doesn't have yet * ban those * Forgot to add code for the marker too * Couple more of these invalid cables were added in master * Update multiple_blood_effects.yml * Update multiple_blood_effects.yml * Fixup maps in TGM format612ca9cbb9: maps/tether/submaps/tether_misc.dmm Automatically commited by: tools\mapmerge2\fixup.py * Fixup now logs the map its currently checking * Final fixes? * Fixup maps in TGM format3078e5cd0a: maps/expedition_vr/beach/submaps/crashedcontainmentshuttle.dmm3078e5cd0a: maps/redgate/fantasy_dungeon.dmm3078e5cd0a: maps/submaps/pois_vr/aerostat/Rockybase.dmm3078e5cd0a: maps/submaps/surface_submaps/mountains/crashedcontainmentshuttle_vr.dmm3078e5cd0a: maps/submaps/surface_submaps/plains/Oldhouse_vr.dmm3078e5cd0a: maps/submaps/surface_submaps/plains/dogbase.dmm3078e5cd0a: maps/submaps/surface_submaps/plains/greatwolfden.dmm3078e5cd0a: maps/submaps/surface_submaps/plains/lonehome_vr.dmm3078e5cd0a: maps/submaps/surface_submaps/plains/methlab.dmm3078e5cd0a: maps/submaps/surface_submaps/plains/oldhotel.dmm3078e5cd0a: maps/submaps/surface_submaps/plains/priderock.dmm3078e5cd0a: maps/submaps/surface_submaps/wilderness/Rockybase.dmm3078e5cd0a: maps/submaps/surface_submaps/wilderness/demonpool.dmm3078e5cd0a: maps/submaps/surface_submaps/wilderness/dogbase.dmm3078e5cd0a: maps/submaps/surface_submaps/wilderness/greatwolfden.dmm3078e5cd0a: maps/tether/submaps/underdark_pois/abandonded_outpost.dmm3078e5cd0a: maps/tether/submaps/underdark_pois/phoron_rat_den.dmm Automatically commited by: tools\mapmerge2\fixup.py * Fix tether_misc error * Remap reused solar farm area * Fix erroneous bearcat entries * Fix weird whitespace (most archive maps also affected but didn't bother) * misc mdb cleanup * moar * grr --------- Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com> Co-authored-by: Cameron Lennox <killer65311@gmail.com>
355 lines
13 KiB
Python
355 lines
13 KiB
Python
import re
|
|
from typing import Optional
|
|
|
|
from .common import Constant, Filename, Typepath
|
|
from .dmm import DMM, Content
|
|
from .error import MaplintError, MapParseError
|
|
|
|
def expect(condition, message):
|
|
if not condition:
|
|
raise MapParseError(message)
|
|
|
|
def fail_content(content: Content, message: str, path_suggestion = "", dm_suggestion = "", dm_sub_suggestion = "") -> MaplintError:
|
|
"""Create an error linked to a specific content instance"""
|
|
return MaplintError(message, content.filename, content.starting_line, path_suggestion, dm_suggestion, dm_sub_suggestion)
|
|
|
|
class TypepathExtra:
|
|
typepath: Typepath
|
|
exact: bool = False
|
|
wildcard: bool = False
|
|
|
|
def __init__(self, typepath):
|
|
if typepath == '*':
|
|
self.wildcard = True
|
|
return
|
|
|
|
if typepath.startswith('='):
|
|
self.exact = True
|
|
typepath = typepath[1:]
|
|
|
|
self.typepath = Typepath(typepath)
|
|
|
|
def matches_path(self, path: Typepath):
|
|
if self.wildcard:
|
|
return True
|
|
|
|
if self.exact:
|
|
return self.typepath == path
|
|
|
|
if len(self.typepath.segments) > len(path.segments):
|
|
return False
|
|
|
|
return self.typepath.segments == path.segments[:len(self.typepath.segments)]
|
|
|
|
class BannedNeighbor:
|
|
identical: bool = False
|
|
typepath: Optional[TypepathExtra] = None
|
|
pattern: Optional[re.Pattern] = None
|
|
|
|
def __init__(self, typepath, data = {}):
|
|
if typepath.upper() != typepath:
|
|
self.typepath = TypepathExtra(typepath)
|
|
|
|
if data is None:
|
|
return
|
|
|
|
expect(isinstance(data, dict), "Banned neighbor must be a dictionary.")
|
|
|
|
if "identical" in data:
|
|
self.identical = data.pop("identical")
|
|
expect(isinstance(self.identical, bool), "identical must be a boolean.")
|
|
|
|
if "pattern" in data:
|
|
self.pattern = re.compile(data.pop("pattern"))
|
|
|
|
expect(len(data) == 0, f"Unknown key in banned neighbor: {', '.join(data.keys())}.")
|
|
|
|
def matches(self, identified: Content, neighbor: Content):
|
|
if self.identical:
|
|
if identified.path != neighbor.path:
|
|
return False
|
|
|
|
if identified.var_edits != neighbor.var_edits:
|
|
return False
|
|
|
|
return True
|
|
|
|
if self.typepath is not None:
|
|
if self.typepath.matches_path(neighbor.path):
|
|
return True
|
|
|
|
if self.pattern is not None:
|
|
if self.pattern.match(str(neighbor.path)):
|
|
return True
|
|
|
|
return False
|
|
|
|
Choices = list[Constant] | re.Pattern
|
|
|
|
def extract_choices(data, key) -> Optional[Choices]:
|
|
if key not in data:
|
|
return None
|
|
|
|
constants_data = data.pop(key)
|
|
|
|
if isinstance(constants_data, list):
|
|
constants: list[Constant] = []
|
|
|
|
for constant_data in constants_data:
|
|
if isinstance(constant_data, str):
|
|
constants.append(constant_data)
|
|
elif isinstance(constant_data, int):
|
|
constants.append(float(constant_data))
|
|
elif isinstance(constant_data, float):
|
|
constants.append(constant_data)
|
|
|
|
return constants
|
|
elif isinstance(constants_data, dict):
|
|
if "pattern" in constants_data:
|
|
pattern = constants_data.pop("pattern")
|
|
return re.compile(pattern)
|
|
|
|
raise MapParseError(f"Unknown key in {key}: {', '.join(constants_data.keys())}.")
|
|
|
|
raise MapParseError(f"{key} must be a list of constants, or a pattern")
|
|
|
|
class BannedVariable:
|
|
variable: str
|
|
allow: Optional[Choices] = None
|
|
deny: Optional[Choices] = None
|
|
|
|
def __init__(self, variable, data = {}):
|
|
self.variable = variable
|
|
|
|
if data is None:
|
|
return
|
|
|
|
self.allow = extract_choices(data, "allow")
|
|
self.deny = extract_choices(data, "deny")
|
|
|
|
expect(len(data) == 0, f"Unknown key in banned variable {variable}: {', '.join(data.keys())}.")
|
|
|
|
def run(self, identified: Content) -> str:
|
|
if identified.var_edits[self.variable] is None:
|
|
return None
|
|
|
|
if self.allow is not None:
|
|
if isinstance(self.allow, list):
|
|
if identified.var_edits[self.variable] not in self.allow:
|
|
return f"Must be one of {', '.join(map(str, self.allow))}"
|
|
elif not self.allow.match(str(identified.var_edits[self.variable])):
|
|
return f"Must match {self.allow.pattern}"
|
|
|
|
return None
|
|
|
|
if self.deny is not None:
|
|
if isinstance(self.deny, list):
|
|
if identified.var_edits[self.variable] in self.deny:
|
|
return f"Must not be one of {', '.join(map(str, self.deny))}"
|
|
elif self.deny.match(str(identified.var_edits[self.variable])):
|
|
return f"Must not match {self.deny.pattern}"
|
|
|
|
return None
|
|
|
|
return f"This variable is not allowed for this type."
|
|
|
|
class Rules:
|
|
banned: bool = False
|
|
banned_neighbors: list[BannedNeighbor] = []
|
|
banned_variables: bool | list[BannedVariable] = []
|
|
|
|
def __init__(self, data):
|
|
expect(isinstance(data, dict), "Lint rules must be a dictionary.")
|
|
|
|
if "banned" in data:
|
|
self.banned = data.pop("banned")
|
|
expect(isinstance(self.banned, bool), "banned must be a boolean.")
|
|
|
|
if "banned_neighbors" in data:
|
|
banned_neighbors_data = data.pop("banned_neighbors")
|
|
|
|
expect(isinstance(banned_neighbors_data, list) or isinstance(banned_neighbors_data, dict), "banned_neighbors must be a list, or a dictionary keyed by type.")
|
|
|
|
if isinstance(banned_neighbors_data, dict):
|
|
self.banned_neighbors = [BannedNeighbor(typepath, data) for typepath, data in banned_neighbors_data.items()]
|
|
else:
|
|
self.banned_neighbors = [BannedNeighbor(typepath) for typepath in banned_neighbors_data]
|
|
|
|
if "banned_variables" in data:
|
|
banned_variables_data = data.pop("banned_variables")
|
|
if banned_variables_data == True:
|
|
self.banned_variables = True
|
|
else:
|
|
expect(isinstance(banned_variables_data, list) or isinstance(banned_variables_data, dict), "banned_variables must be a list, or a dictionary keyed by variable.")
|
|
|
|
if isinstance(banned_variables_data, dict):
|
|
self.banned_variables = [BannedVariable(variable, data) for variable, data in banned_variables_data.items()]
|
|
else:
|
|
self.banned_variables = [BannedVariable(variable) for variable in banned_variables_data]
|
|
|
|
expect(len(data) == 0, f"Unknown lint rules: {', '.join(data.keys())}.")
|
|
|
|
def run(self, identified: Content, contents: list[Content], identified_index) -> list[MaplintError]:
|
|
failures: list[MaplintError] = []
|
|
|
|
if self.banned:
|
|
failures.append(fail_content(identified, f"Typepath {identified.path} is banned."))
|
|
|
|
for banned_neighbor in self.banned_neighbors:
|
|
for neighbor in contents[:identified_index] + contents[identified_index + 1:]:
|
|
if not banned_neighbor.matches(identified, neighbor):
|
|
continue
|
|
|
|
failures.append(fail_content(identified, f"Typepath {identified.path} has a banned neighbor: {neighbor.path}"))
|
|
|
|
if self.banned_variables == True:
|
|
if len(identified.var_edits) > 0:
|
|
path_suggestion, dm_suggestion, dm_sub_suggestion = self.parse_suggestion(identified)
|
|
failures.append(fail_content(identified, f"Typepath {identified.path} should not have any variable edits.", path_suggestion, dm_suggestion, dm_sub_suggestion))
|
|
else:
|
|
assert isinstance(self.banned_variables, list)
|
|
for banned_variable in self.banned_variables:
|
|
if banned_variable.variable in identified.var_edits:
|
|
ban_reason = banned_variable.run(identified)
|
|
if ban_reason is None:
|
|
continue
|
|
failures.append(fail_content(identified, f"Typepath {identified.path} has a banned variable (set to {identified.var_edits[banned_variable.variable]}): {banned_variable.variable}. {ban_reason}"))
|
|
|
|
return failures
|
|
|
|
def parse_suggestion(self, identified):
|
|
#figure out what typepath we're going to suggest
|
|
other_var_count = 0
|
|
typepath_suggestion = f"{identified.path}/"
|
|
dir_var = ""
|
|
for var_name, var_value in identified.var_edits.items():
|
|
if(var_name == "dir"):
|
|
dir_var = self.parse_direction(var_value)
|
|
else:
|
|
other_var_count += 1
|
|
if(other_var_count > 1):
|
|
typepath_suggestion += "_"
|
|
typepath_suggestion += f"{var_value}"
|
|
|
|
#cleanup typepath
|
|
typepath_suggestion = typepath_suggestion.replace(" ", "_").replace("-", "_").replace(",", "_")
|
|
|
|
#always offer a unique dir as a subtype
|
|
typepath_dir_suggestion = ""
|
|
if(dir_var):
|
|
if(other_var_count > 0):
|
|
typepath_dir_suggestion = f"{typepath_suggestion}/{dir_var}"
|
|
else:
|
|
typepath_suggestion += dir_var
|
|
|
|
#generate suggestion entries
|
|
dm_suggestion = f"{typepath_suggestion}\n"
|
|
path_suggestion = f"{identified.path}{{"
|
|
for var_name, var_value in identified.var_edits.items():
|
|
if(var_name == "dir"):
|
|
if(typepath_dir_suggestion == ""):
|
|
dm_suggestion += f"\t{var_name} = {dir_var.upper()}\n"
|
|
path_suggestion += f"{var_name}={var_value};"
|
|
elif(isinstance(var_value, Filename)):
|
|
dm_suggestion += f"\t{var_name} = \'{var_value.path}\'\n"
|
|
path_suggestion += f"{var_name}=\'{var_value.path}\';"
|
|
elif(isinstance(var_value, str)):
|
|
dm_suggestion += f"\t{var_name} = \"{var_value}\"\n"
|
|
path_suggestion += f"{var_name}=\"{var_value}\";"
|
|
else:
|
|
dm_suggestion += f"\t{var_name} = {var_value}\n"
|
|
path_suggestion += f"{var_name}={var_value};"
|
|
dm_suggestion += "\n"
|
|
if(typepath_dir_suggestion == ""):
|
|
path_suggestion += f"}} : {typepath_suggestion}\n"
|
|
|
|
#generate second dm suggestion if its a dir with other stuff
|
|
dm_sub_suggestion = ""
|
|
if(typepath_dir_suggestion != ""):
|
|
dm_sub_suggestion = dm_suggestion
|
|
dm_suggestion = f"{typepath_dir_suggestion}\n"
|
|
dm_suggestion += f"\tdir = {dir_var.upper()}\n\n"
|
|
path_suggestion += f"}} : {typepath_dir_suggestion}\n"
|
|
|
|
return path_suggestion, dm_suggestion, dm_sub_suggestion
|
|
|
|
def parse_direction(self, number):
|
|
if(number == 1):
|
|
return "north"
|
|
if(number == 2):
|
|
return "south"
|
|
if(number == 4):
|
|
return "east"
|
|
if(number == 8):
|
|
return "west"
|
|
if(number == 5):
|
|
return "northeast"
|
|
if(number == 6):
|
|
return "southeast"
|
|
if(number == 9):
|
|
return "northwest"
|
|
if(number == 10):
|
|
return "southwest"
|
|
if(number == 16):
|
|
return "up"
|
|
if(number == 32):
|
|
return "down"
|
|
raise TypeError(f"Unknown direction : {number}")
|
|
|
|
class Lint:
|
|
help: Optional[str] = None
|
|
rules: dict[TypepathExtra, Rules]
|
|
|
|
def __init__(self, data):
|
|
expect(isinstance(data, dict), "Lint must be a dictionary.")
|
|
|
|
if "help" in data:
|
|
self.help = data.pop("help")
|
|
|
|
expect(isinstance(self.help, str) or self.help is None, "Lint help must be a string.")
|
|
|
|
self.rules = {}
|
|
|
|
for typepath, rules in data.items():
|
|
self.rules[TypepathExtra(typepath)] = Rules(rules)
|
|
|
|
def run(self, map_data: DMM) -> list[MaplintError]:
|
|
all_failures: list[MaplintError] = []
|
|
(width, height) = map_data.size()
|
|
|
|
for pop, contents in map_data.pops.items():
|
|
for typepath_extra, rules in self.rules.items():
|
|
for content_index, content in enumerate(contents):
|
|
if not typepath_extra.matches_path(content.path):
|
|
continue
|
|
|
|
failures = rules.run(content, contents, content_index)
|
|
if len(failures) == 0:
|
|
continue
|
|
|
|
coordinates = map_data.turfs_for_pop(pop)
|
|
coordinate_texts = []
|
|
|
|
for _ in range(3):
|
|
coordinate = next(coordinates, None)
|
|
if coordinate is None:
|
|
break
|
|
|
|
x = coordinate[0] + 1
|
|
y = height - coordinate[1]
|
|
z = coordinate[2] + 1
|
|
|
|
coordinate_texts.append(f"({x}, {y}, {z})")
|
|
|
|
leftover_coordinates = sum(1 for _ in coordinates)
|
|
if leftover_coordinates > 0:
|
|
coordinate_texts.append(f"and {leftover_coordinates} more")
|
|
|
|
for failure in failures:
|
|
failure.coordinates = ', '.join(coordinate_texts)
|
|
failure.help = self.help
|
|
failure.pop_id = pop
|
|
all_failures.append(failure)
|
|
|
|
return list(set(all_failures))
|