diff --git a/tools/hooks/install.bat b/tools/hooks/install.bat index c4f864b5c68..7a11129a2a2 100644 --- a/tools/hooks/install.bat +++ b/tools/hooks/install.bat @@ -10,5 +10,7 @@ for %%f in (*.merge) do ( driver = tools/hooks/%%f %%P %%O %%A %%B %%L >> ..\..\.git\config ) +echo Installing Python dependencies +python -m pip install -r ..\mapmerge2\requirements.txt echo Done pause diff --git a/tools/hooks/install.sh b/tools/hooks/install.sh index 32183a7ce89..b0273834481 100644 --- a/tools/hooks/install.sh +++ b/tools/hooks/install.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -e shopt -s nullglob cd "$(dirname "$0")" for f in *.hook; do @@ -9,4 +10,8 @@ for f in *.merge; do echo Installing merge driver: ${f%.merge} git config --replace-all merge.${f%.merge}.driver "tools/hooks/$f %P %O %A %B %L" done + +echo "Installing Python dependencies" +./python.sh -m pip install -r ../mapmerge2/requirements.txt + echo "Done" diff --git a/tools/mapmerge2/README.md b/tools/mapmerge2/README.md index eaa1a6dfff7..0ff4d21ac2e 100644 --- a/tools/mapmerge2/README.md +++ b/tools/mapmerge2/README.md @@ -15,28 +15,15 @@ contains the desired changes. ## Installation -To install Python dependencies, run `requirements-install.bat`, OR run -`python -m pip install -r requirements.txt` directly. Make sure you have Python 3.5 -or higher before doing so. +To install Python dependencies, run `requirements-install.bat`, or run +`python -m pip install -r requirements.txt` directly. See the [Git hooks] +documentation to install the Git pre-commit hook which runs the map merger +automatically, or use `tools/mapmerge/Prepare Maps.bat` to save backups before +running `mapmerge.bat`. For up-to-date installation and detailed troubleshooting instructions, visit the [Map Merger] wiki article. -## Usage - -Make sure you've performed the installation steps above! You have two choices for using this tool: Either view the [Git hooks] documentation to install the hook to automate this process, -or to perform merges manually, follow the steps below. - -1. Run Prepare Maps.bat as this provides backups before making changes to a map. It also makes sure the mapmerger actually works. - -2. Edit your map and save. Close DM.** - -3. Run mapmerge.bat - -4. Commit your changes and you're done! - -**Note: Do not open the map in dreammaker before committing the results of mapmerger - this can cause dreammaker to save back into the default dmm format. If you're having issues with your map getting stuck in dmm mode, try committing and pushing the mapmerger changes before reopening in dreammaker. - ## Code Structure Frontend scripts are meant to be run directly. They obey the environment diff --git a/tools/mapmerge2/dmm.py b/tools/mapmerge2/dmm.py index 8bb1dca3537..f66f2b8edf1 100644 --- a/tools/mapmerge2/dmm.py +++ b/tools/mapmerge2/dmm.py @@ -343,7 +343,7 @@ def _parse(map_raw_text): in_map_block = False in_coord_block = False in_map_string = False - iter_x = 0 + base_x = 0 adjust_y = True curr_num = "" @@ -487,7 +487,7 @@ def _parse(map_raw_text): curr_x = int(curr_num) if curr_x > maxx: maxx = curr_x - iter_x = 0 + base_x = curr_x curr_num = "" reading_coord = "y" elif reading_coord == "y": @@ -521,21 +521,15 @@ def _parse(map_raw_text): adjust_y = False else: curr_y += 1 - if curr_x > maxx: - maxx = curr_x - if iter_x > 1: - curr_x = 1 - iter_x = 0 - + curr_x = base_x else: curr_key = BASE * curr_key + base52_r[char] curr_key_len += 1 if curr_key_len == key_length: - iter_x += 1 - if iter_x > 1: - curr_x += 1 - grid[curr_x, curr_y, curr_z] = duplicate_keys.get(curr_key, curr_key) + if curr_x > maxx: + maxx = curr_x + curr_x += 1 curr_key = 0 curr_key_len = 0 diff --git a/tools/mapmerge2/requirements.txt b/tools/mapmerge2/requirements.txt index ef180d7b95d..41ddc96c174 100644 --- a/tools/mapmerge2/requirements.txt +++ b/tools/mapmerge2/requirements.txt @@ -1,3 +1,3 @@ -pygit2>=0.27.2 -bidict>=0.13.1 -Pillow>=5.1.0 +pygit2==1.0.1 +bidict==0.13.1 +Pillow==7.0.0 diff --git a/tools/mapmerge2/update_paths.py b/tools/mapmerge2/update_paths.py new file mode 100644 index 00000000000..2c316e941de --- /dev/null +++ b/tools/mapmerge2/update_paths.py @@ -0,0 +1,176 @@ +# A script and syntax for applying path updates to maps. +import re +import os +import argparse +import frontend +from dmm import * + +desc = """ +Update dmm files given update file/string. +Replacement syntax example: + /turf/open/floor/plasteel/warningline : /obj/effect/turf_decal {dir = @OLD ;tag = @SKIP;icon_state = @SKIP} + /turf/open/floor/plasteel/warningline : /obj/effect/turf_decal {@OLD} , /obj/thing {icon_state = @OLD:name; name = "meme"} + /turf/open/floor/plasteel/warningline{dir=2} : /obj/thing +New paths properties: + @OLD - if used as property name copies all modified properties from original path to this one + property = @SKIP - will not copy this property through when global @OLD is used. + property = @OLD - will copy this modified property from original object even if global @OLD is not used + property = @OLD:name - will copy [name] property from original object even if global @OLD is not used + Anything else is copied as written. +Old paths properties: + Will be used as a filter. + property = @UNSET - will apply the rule only if the property is not mapedited +""" + +default_map_directory = "../../_maps" +replacement_re = re.compile(r'\s*(?P[^{]*)\s*(\{(?P.*)\})?') + +#urgent todo: replace with actual parser, this is slow as janitor in crit +split_re = re.compile(r'((?:[A-Za-z0-9_\-$]+)\s*=\s*(?:"(?:.+?)"|[^";]*)|@OLD)') + + +def props_to_string(props): + return "{{{}}}".format(";".join([f"{k} = {v}" for k, v in props.items()])) + + +def string_to_props(propstring, verbose = False): + props = dict() + for raw_prop in re.split(split_re, propstring): + if not raw_prop or raw_prop.strip() == ';': + continue + prop = raw_prop.split('=', maxsplit=1) + props[prop[0].strip()] = prop[1].strip() if len(prop) > 1 else None + if verbose: + print("{0} to {1}".format(propstring, props)) + return props + + +def parse_rep_string(replacement_string, verbose = False): + # translates /blah/blah {meme = "test",} into path,prop dictionary tuple + match = re.match(replacement_re, replacement_string) + path = match['path'] + props = match['props'] + if props: + prop_dict = string_to_props(props, verbose) + else: + prop_dict = dict() + return path.strip(), prop_dict + + +def update_path(dmm_data, replacement_string, verbose=False): + old_path_part, new_path_part = replacement_string.split(':', maxsplit=1) + old_path, old_path_props = parse_rep_string(old_path_part, verbose) + new_paths = list() + for replacement_def in new_path_part.split(','): + new_path, new_path_props = parse_rep_string(replacement_def, verbose) + new_paths.append((new_path, new_path_props)) + + subtypes = "" + if old_path.endswith("/@SUBTYPES"): + old_path = old_path[:-len("/@SUBTYPES")] + if verbose: + print("Looking for subtypes of", old_path) + subtypes = r"(?:/\w+)*" + + replacement_pattern = re.compile(rf"(?P{re.escape(old_path)}{subtypes})\s*(:?{{(?P.*)}})?$") + + def replace_def(match): + if match['props']: + old_props = string_to_props(match['props'], verbose) + else: + old_props = dict() + for filter_prop in old_path_props: + if filter_prop not in old_props: + if old_path_props[filter_prop] == "@UNSET": + continue + else: + return [match.group(0)] + else: + if old_props[filter_prop] != old_path_props[filter_prop] or old_path_props[filter_prop] == "@UNSET": + return [match.group(0)] #does not match current filter, skip the change. + if verbose: + print("Found match : {0}".format(match.group(0))) + out_paths = [] + for new_path, new_props in new_paths: + if new_path == "@OLD": + out = match.group('path') + else: + out = new_path + out_props = dict() + for prop_name, prop_value in new_props.items(): + if prop_name == "@OLD": + out_props = dict(old_props) + continue + if prop_value == "@SKIP": + out_props.pop(prop_name, None) + continue + if prop_value.startswith("@OLD"): + params = prop_value.split(":") + if prop_name in old_props: + out_props[prop_name] = old_props[params[1]] if len(params) > 1 else old_props[prop_name] + continue + out_props[prop_name] = prop_value + if out_props: + out += props_to_string(out_props) + out_paths.append(out) + if verbose: + print("Replacing with: {0}".format(out_paths)) + return out_paths + + def get_result(element): + match = replacement_pattern.match(element) + if match: + return replace_def(match) + else: + return [element] + + bad_keys = {} + keys = list(dmm_data.dictionary.keys()) + for definition_key in keys: + def_value = dmm_data.dictionary[definition_key] + new_value = tuple(y for x in def_value for y in get_result(x)) + if new_value != def_value: + dmm_data.overwrite_key(definition_key, new_value, bad_keys) + dmm_data.reassign_bad_keys(bad_keys) + + +def update_map(map_filepath, updates, verbose=False): + print("Updating: {0}".format(map_filepath)) + dmm_data = DMM.from_file(map_filepath) + for update_string in updates: + update_path(dmm_data, update_string, verbose) + dmm_data.to_file(map_filepath, True) + + +def update_all_maps(map_directory, updates, verbose=False): + for root, _, files in os.walk(map_directory): + for filepath in files: + if filepath.endswith(".dmm"): + path = os.path.join(root, filepath) + update_map(path, updates, verbose) + + +def main(args): + if args.inline: + print("Using replacement:", args.update_source) + updates = [args.update_source] + else: + with open(args.update_source) as f: + updates = [line for line in f if line and not line.startswith("#") and not line.isspace()] + print(f"Using {len(updates)} replacements from file:", args.update_source) + + if args.map: + update_map(args.map, updates, verbose=args.verbose) + else: + map_directory = args.directory or frontend.read_settings().map_folder + update_all_maps(map_directory, updates, verbose=args.verbose) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=desc, formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument("update_source", help="update file path / line of update notation") + parser.add_argument("--map", "-m", help="path to update, defaults to all maps in maps directory") + parser.add_argument("--directory", "-d", help="path to maps directory, defaults to _maps/") + parser.add_argument("--inline", "-i", help="treat update source as update string instead of path", action="store_true") + parser.add_argument("--verbose", "-v", help="toggle detailed update information", action="store_true") + main(parser.parse_args())