From ae08a6896f07ef20b17ae776fed90e61c9174de3 Mon Sep 17 00:00:00 2001 From: Arokha Sieyes Date: Sat, 27 Jan 2018 23:38:52 -0500 Subject: [PATCH] Add MapMerge2 from /tg/ --- tools/mapmerge2/README.md | 52 ++ tools/mapmerge2/README.txt | 52 ++ tools/mapmerge2/convert.py | 8 + tools/mapmerge2/dmm.py | 459 +++++++++++++++ tools/mapmerge2/dmm2tgm.bat | 5 + tools/mapmerge2/frontend.py | 127 +++++ tools/mapmerge2/hooks/README.md | 36 ++ tools/mapmerge2/hooks/install.bat | 15 + tools/mapmerge2/hooks/install.sh | 11 + tools/mapmerge2/hooks/pre-commit.hook | 2 + tools/mapmerge2/hooks/python.sh | 17 + tools/mapmerge2/mapmerge.bat | 5 + tools/mapmerge2/mapmerge.py | 94 ++++ tools/mapmerge2/precommit.py | 50 ++ tools/mapmerge2/requirements-install.bat | 3 + tools/mapmerge2/requirements.txt | 2 + tools/mapmerge2/tgm2dmm.bat | 5 + tools/mapmergepy/1 Prepare Maps.bat | 12 - tools/mapmergepy/2 Clean Maps.bat | 4 - .../__pycache__/map_helpers.cpython-36.pyc | Bin 7998 -> 0 bytes tools/mapmergepy/map_helpers.py | 524 ------------------ tools/mapmergepy/mapmerger.py | 81 --- tools/mapmergepy/prepare_maps.sh | 2 - 23 files changed, 943 insertions(+), 623 deletions(-) create mode 100644 tools/mapmerge2/README.md create mode 100644 tools/mapmerge2/README.txt create mode 100644 tools/mapmerge2/convert.py create mode 100644 tools/mapmerge2/dmm.py create mode 100644 tools/mapmerge2/dmm2tgm.bat create mode 100644 tools/mapmerge2/frontend.py create mode 100644 tools/mapmerge2/hooks/README.md create mode 100644 tools/mapmerge2/hooks/install.bat create mode 100644 tools/mapmerge2/hooks/install.sh create mode 100644 tools/mapmerge2/hooks/pre-commit.hook create mode 100644 tools/mapmerge2/hooks/python.sh create mode 100644 tools/mapmerge2/mapmerge.bat create mode 100644 tools/mapmerge2/mapmerge.py create mode 100644 tools/mapmerge2/precommit.py create mode 100644 tools/mapmerge2/requirements-install.bat create mode 100644 tools/mapmerge2/requirements.txt create mode 100644 tools/mapmerge2/tgm2dmm.bat delete mode 100644 tools/mapmergepy/1 Prepare Maps.bat delete mode 100644 tools/mapmergepy/2 Clean Maps.bat delete mode 100644 tools/mapmergepy/__pycache__/map_helpers.cpython-36.pyc delete mode 100644 tools/mapmergepy/map_helpers.py delete mode 100644 tools/mapmergepy/mapmerger.py delete mode 100644 tools/mapmergepy/prepare_maps.sh diff --git a/tools/mapmerge2/README.md b/tools/mapmerge2/README.md new file mode 100644 index 00000000000..0ff4d21ac2e --- /dev/null +++ b/tools/mapmerge2/README.md @@ -0,0 +1,52 @@ +# Map Merge 2 + +**Map Merge 2** is an improvement over previous map merging scripts, with +better merge-conflict prevention, multi-Z support, and automatic handling of +key overflow. For up-to-date tips and tricks, also visit the [Map Merger] wiki article. + +## What Map Merging Is + +The "map merge" operation describes the process of rewriting a map file written +by the DreamMaker map editor to A) use a format more amenable to Git's conflict +resolution and B) differ in the least amount textually from the previous +version of the map while maintaining all the actual changes. It requires an old +version of the map to use as a reference and a new version of the map which +contains the desired changes. + +## Installation + +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. + +## Code Structure + +Frontend scripts are meant to be run directly. They obey the environment +variables `TGM` to set whether files are saved in TGM (1) or DMM (0) format, +and `MAPROOT` to determine where maps are kept. By default, TGM is used and +the map root is autodetected. Each script may either prompt for the desired map +or be run with command-line parameters indicating which maps to act on. The +scripts include: + +* `convert.py` for converting maps to and from the TGM format. Used by + `tgm2dmm.bat` and `dmm2tgm.bat`. +* `mapmerge.py` for running the map merge on map backups saved by + `Prepare Maps.bat`. Used by `mapmerge.bat` + +Implementation modules: + +* `dmm.py` includes the map reader and writer. +* `mapmerge.py` includes the implementation of the map merge operation. +* `frontend.py` includes the common code for the frontend scripts. + +`precommit.py` is run by the [Git hooks] if installed, and merges the new +version of any map saved in the index (`git add`ed) with the old version stored +in Git when run. + +[Map Merger]: https://tgstation13.org/wiki/Map_Merger +[Git hooks]: ../hooks/README.md diff --git a/tools/mapmerge2/README.txt b/tools/mapmerge2/README.txt new file mode 100644 index 00000000000..0ff4d21ac2e --- /dev/null +++ b/tools/mapmerge2/README.txt @@ -0,0 +1,52 @@ +# Map Merge 2 + +**Map Merge 2** is an improvement over previous map merging scripts, with +better merge-conflict prevention, multi-Z support, and automatic handling of +key overflow. For up-to-date tips and tricks, also visit the [Map Merger] wiki article. + +## What Map Merging Is + +The "map merge" operation describes the process of rewriting a map file written +by the DreamMaker map editor to A) use a format more amenable to Git's conflict +resolution and B) differ in the least amount textually from the previous +version of the map while maintaining all the actual changes. It requires an old +version of the map to use as a reference and a new version of the map which +contains the desired changes. + +## Installation + +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. + +## Code Structure + +Frontend scripts are meant to be run directly. They obey the environment +variables `TGM` to set whether files are saved in TGM (1) or DMM (0) format, +and `MAPROOT` to determine where maps are kept. By default, TGM is used and +the map root is autodetected. Each script may either prompt for the desired map +or be run with command-line parameters indicating which maps to act on. The +scripts include: + +* `convert.py` for converting maps to and from the TGM format. Used by + `tgm2dmm.bat` and `dmm2tgm.bat`. +* `mapmerge.py` for running the map merge on map backups saved by + `Prepare Maps.bat`. Used by `mapmerge.bat` + +Implementation modules: + +* `dmm.py` includes the map reader and writer. +* `mapmerge.py` includes the implementation of the map merge operation. +* `frontend.py` includes the common code for the frontend scripts. + +`precommit.py` is run by the [Git hooks] if installed, and merges the new +version of any map saved in the index (`git add`ed) with the old version stored +in Git when run. + +[Map Merger]: https://tgstation13.org/wiki/Map_Merger +[Git hooks]: ../hooks/README.md diff --git a/tools/mapmerge2/convert.py b/tools/mapmerge2/convert.py new file mode 100644 index 00000000000..35e5dda4433 --- /dev/null +++ b/tools/mapmerge2/convert.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +import frontend +import dmm + +if __name__ == '__main__': + settings = frontend.read_settings() + for fname in frontend.process(settings, "convert"): + dmm.DMM.from_file(fname).to_file(fname, settings.tgm) diff --git a/tools/mapmerge2/dmm.py b/tools/mapmerge2/dmm.py new file mode 100644 index 00000000000..155181d413a --- /dev/null +++ b/tools/mapmerge2/dmm.py @@ -0,0 +1,459 @@ +# Tools for working with DreamMaker maps + +import io +import bidict +import random +from collections import namedtuple + +TGM_HEADER = "//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE" +ENCODING = 'utf-8' + +Coordinate = namedtuple('Coordinate', ['x', 'y', 'z']) + +class DMM: + __slots__ = ['key_length', 'size', 'dictionary', 'grid', 'header'] + + def __init__(self, key_length, size): + self.key_length = key_length + self.size = size + self.dictionary = bidict.bidict() + self.grid = {} + self.header = None + + @staticmethod + def from_file(fname): + # stream the file rather than forcing all its contents to memory + with open(fname, 'r', encoding=ENCODING) as f: + return _parse(iter(lambda: f.read(1), '')) + + @staticmethod + def from_bytes(bytes): + return _parse(bytes.decode(ENCODING)) + + def to_file(self, fname, tgm = True): + with open(fname, 'w', newline='\n', encoding=ENCODING) as f: + (save_tgm if tgm else save_dmm)(self, f) + + def to_bytes(self, tgm = True): + bio = io.BytesIO() + with io.TextIOWrapper(bio, newline='\n', encoding=ENCODING) as f: + (save_tgm if tgm else save_dmm)(self, f) + f.flush() + return bio.getvalue() + + def generate_new_key(self): + # ensure that free keys exist by increasing the key length if necessary + free_keys = (BASE ** self.key_length) - len(self.dictionary) + while free_keys <= 0: + self.key_length += 1 + free_keys = (BASE ** self.key_length) - len(self.dictionary) + + # choose one of the free keys at random + key = 0 + while free_keys: + if key not in self.dictionary: + # this construction is used to avoid needing to construct the + # full set in order to random.choice() from it + if random.random() < 1 / free_keys: + return key + free_keys -= 1 + key += 1 + + raise RuntimeError("ran out of keys, this shouldn't happen") + + @property + def coords_zyx(self): + for z in range(1, self.size.z + 1): + for y in range(1, self.size.y + 1): + for x in range(1, self.size.x + 1): + yield (z, y, x) + + @property + def coords_z(self): + return range(1, self.size.z + 1) + + @property + def coords_yx(self): + for y in range(1, self.size.y + 1): + for x in range(1, self.size.x + 1): + yield (y, x) + +# ---------- +# key handling + +# Base 52 a-z A-Z dictionary for fast conversion +BASE = 52 +base52 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' +base52_r = {x: i for i, x in enumerate(base52)} +assert len(base52) == BASE and len(base52_r) == BASE + +def key_to_num(key): + num = 0 + for ch in key: + num = BASE * num + base52_r[ch] + return num + +def num_to_key(num, key_length): + if num >= BASE ** key_length: + raise KeyTooLarge(f"num={num} does not fit in key_length={key_length}") + + result = '' + while num: + result = base52[num % BASE] + result + num //= BASE + + assert len(result) <= key_length + return base52[0] * (key_length - len(result)) + result + +class KeyTooLarge(Exception): + pass + +# ---------- +# An actual atom parser + +def parse_map_atom(atom): + try: + i = atom.index('{') + except ValueError: + return atom, {} + + path, rest = atom[:i], atom[i+1:] + vars = {} + + in_string = False + in_name = False + escaping = False + current_name = '' + current = '' + for ch in rest: + if escaping: + escaping = False + current += ch + elif ch == '\\': + escaping = True + elif ch == '"': + in_string = not in_string + current += ch + elif in_string: + current += ch + elif ch == ';': + vars[current_name.strip()] = current.strip() + current_name = current = '' + elif ch == '=': + current_name = current + current = '' + elif ch == '}': + vars[current_name.strip()] = current.strip() + break + elif ch not in ' ': + current += ch + + return path, vars + +# ---------- +# TGM writer + +def save_tgm(dmm, output): + output.write(f"{TGM_HEADER}\n") + if dmm.header: + output.write(f"{dmm.header}\n") + + # write dictionary in tgm format + for key, value in sorted(dmm.dictionary.items()): + output.write(f'"{num_to_key(key, dmm.key_length)}" = (\n') + for idx, thing in enumerate(value): + in_quote_block = False + in_varedit_block = False + for char in thing: + if in_quote_block: + if char == '"': + in_quote_block = False + output.write(char) + elif char == '"': + in_quote_block = True + output.write(char) + elif not in_varedit_block: + if char == "{": + in_varedit_block = True + output.write("{\n\t") + else: + output.write(char) + elif char == ";": + output.write(";\n\t") + elif char == "}": + output.write("\n\t}") + in_varedit_block = False + else: + output.write(char) + if idx < len(value) - 1: + output.write(",\n") + output.write(")\n") + + # thanks to YotaXP for finding out about this one + max_x, max_y, max_z = dmm.size + for z in range(1, max_z + 1): + output.write("\n") + for x in range(1, max_x + 1): + output.write(f"({x},{1},{z}) = {{\"\n") + for y in range(1, max_y + 1): + output.write(f"{num_to_key(dmm.grid[x, y, z], dmm.key_length)}\n") + output.write("\"}\n") + +# ---------- +# DMM writer + +def save_dmm(dmm, output): + if dmm.header: + output.write(f"{dmm.header}\n") + + # writes a tile dictionary the same way Dreammaker does + for key, value in sorted(dmm.dictionary.items()): + output.write(f'"{num_to_key(key, dmm.key_length)}" = ({",".join(value)})\n') + + output.write("\n") + + # writes a map grid the same way Dreammaker does + max_x, max_y, max_z = dmm.size + for z in range(1, max_z + 1): + output.write(f"(1,1,{z}) = {{\"\n") + + for y in range(1, max_y + 1): + for x in range(1, max_x + 1): + try: + output.write(num_to_key(dmm.grid[x, y, z], dmm.key_length)) + except KeyError: + print(f"Key error: ({x}, {y}, {z})") + output.write("\n") + output.write("\"}\n") + +# ---------- +# Parser + +def _parse(map_raw_text): + in_comment_line = False + comment_trigger = False + + in_quote_block = False + in_key_block = False + in_data_block = False + in_varedit_block = False + after_data_block = False + escaping = False + skip_whitespace = False + + dictionary = bidict.bidict() + duplicate_keys = {} + curr_key_len = 0 + curr_key = 0 + curr_datum = "" + curr_data = list() + + in_map_block = False + in_coord_block = False + in_map_string = False + iter_x = 0 + adjust_y = True + + curr_num = "" + reading_coord = "x" + + key_length = 0 + + maxx = 0 + maxy = 0 + maxz = 0 + + curr_x = 0 + curr_y = 0 + curr_z = 0 + grid = dict() + + it = iter(map_raw_text) + + # map block + for char in it: + if char == "\n": + in_comment_line = False + comment_trigger = False + continue + elif in_comment_line: + continue + elif char == "\t": + continue + + if char == "/" and not in_quote_block: + if comment_trigger: + in_comment_line = True + continue + else: + comment_trigger = True + else: + comment_trigger = False + + if in_data_block: + + if in_varedit_block: + + if in_quote_block: + if char == "\\": + curr_datum = curr_datum + char + escaping = True + + elif escaping: + curr_datum = curr_datum + char + escaping = False + + elif char == "\"": + curr_datum = curr_datum + char + in_quote_block = False + + else: + curr_datum = curr_datum + char + + else: + if skip_whitespace and char == " ": + skip_whitespace = False + continue + skip_whitespace = False + + if char == "\"": + curr_datum = curr_datum + char + in_quote_block = True + + elif char == ";": + skip_whitespace = True + curr_datum = curr_datum + char + + elif char == "}": + curr_datum = curr_datum + char + in_varedit_block = False + + else: + curr_datum = curr_datum + char + + elif char == "{": + curr_datum = curr_datum + char + in_varedit_block = True + + elif char == ",": + curr_data.append(curr_datum) + curr_datum = "" + + elif char == ")": + curr_data.append(curr_datum) + curr_data = tuple(curr_data) + try: + dictionary[curr_key] = curr_data + except bidict.ValueDuplicationError: + # if the map has duplicate values, eliminate them now + duplicate_keys[curr_key] = dictionary.inv[curr_data] + curr_data = list() + curr_datum = "" + curr_key = 0 + curr_key_len = 0 + in_data_block = False + after_data_block = True + + else: + curr_datum = curr_datum + char + + elif in_key_block: + if char == "\"": + in_key_block = False + if key_length == 0: + key_length = curr_key_len + else: + assert key_length == curr_key_len + else: + curr_key = BASE * curr_key + base52_r[char] + curr_key_len += 1 + + # else we're looking for a key block, a data block or the map block + elif char == "\"": + in_key_block = True + after_data_block = False + + elif char == "(": + if after_data_block: + in_coord_block = True + after_data_block = False + curr_key = 0 + curr_key_len = 0 + break + else: + in_data_block = True + after_data_block = False + + # grid block + for char in it: + if in_coord_block: + if char == ",": + if reading_coord == "x": + curr_x = int(curr_num) + if curr_x > maxx: + maxx = curr_x + iter_x = 0 + curr_num = "" + reading_coord = "y" + elif reading_coord == "y": + curr_y = int(curr_num) + if curr_y > maxy: + maxy = curr_y + curr_num = "" + reading_coord = "z" + else: + raise ValueError("too many dimensions") + + elif char == ")": + curr_z = int(curr_num) + if curr_z > maxz: + maxz = curr_z + in_coord_block = False + reading_coord = "x" + curr_num = "" + + else: + curr_num = curr_num + char + + elif in_map_string: + if char == "\"": + in_map_string = False + adjust_y = True + curr_y -= 1 + + elif char == "\n": + if adjust_y: + adjust_y = False + else: + curr_y += 1 + if curr_x > maxx: + maxx = curr_x + if iter_x > 1: + curr_x = 1 + iter_x = 0 + + 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) + curr_key = 0 + curr_key_len = 0 + + # else look for coordinate block or a map string + elif char == "(": + in_coord_block = True + elif char == "\"": + in_map_string = True + + if curr_y > maxy: + maxy = curr_y + + data = DMM(key_length, Coordinate(maxx, maxy, maxz)) + data.dictionary = dictionary + data.grid = grid + return data diff --git a/tools/mapmerge2/dmm2tgm.bat b/tools/mapmerge2/dmm2tgm.bat new file mode 100644 index 00000000000..bcf6150c2ea --- /dev/null +++ b/tools/mapmerge2/dmm2tgm.bat @@ -0,0 +1,5 @@ +@echo off +set MAPROOT=../../_maps/ +set TGM=1 +python convert.py +pause diff --git a/tools/mapmerge2/frontend.py b/tools/mapmerge2/frontend.py new file mode 100644 index 00000000000..4d44eab951e --- /dev/null +++ b/tools/mapmerge2/frontend.py @@ -0,0 +1,127 @@ +# Common code for the frontend interface of map tools +import sys +import os +import pathlib +import shutil +from collections import namedtuple + +Settings = namedtuple('Settings', ['map_folder', 'tgm']) +MapsToRun = namedtuple('MapsToRun', ['files', 'indices']) + +def string_to_num(s): + try: + return int(s) + except ValueError: + return -1 + +def read_settings(): + # discover map folder if needed + try: + map_folder = os.environ['MAPROOT'] + except KeyError: + map_folder = '_maps/' + for _ in range(8): + if os.path.exists(map_folder): + break + map_folder = os.path.join('..', map_folder) + else: + map_folder = None + + # assume TGM is True by default + tgm = os.environ.get('TGM', "1") == "1" + + return Settings(map_folder, tgm) + +def pretty_path(settings, path_str): + if settings.map_folder: + return path_str[len(os.path.commonpath([settings.map_folder, path_str]))+1:] + else: + return path_str + +def prompt_maps(settings, verb): + if not settings.map_folder: + print("Could not autodetect the _maps folder, set MAPROOT") + exit(1) + + list_of_files = list() + for root, directories, filenames in os.walk(settings.map_folder): + for filename in [f for f in filenames if f.endswith(".dmm")]: + list_of_files.append(pathlib.Path(root, filename)) + + last_dir = "" + for i, this_file in enumerate(list_of_files): + this_dir = this_file.parent + if last_dir != this_dir: + print("--------------------------------") + last_dir = this_dir + print("[{}]: {}".format(i, pretty_path(settings, str(this_file)))) + + print("--------------------------------") + in_list = input("List the maps you want to " + verb + " (example: 1,3-5,12):\n") + in_list = in_list.replace(" ", "") + in_list = in_list.split(",") + + valid_indices = list() + for m in in_list: + index_range = m.split("-") + if len(index_range) == 1: + index = string_to_num(index_range[0]) + if index >= 0 and index < len(list_of_files): + valid_indices.append(index) + elif len(index_range) == 2: + index0 = string_to_num(index_range[0]) + index1 = string_to_num(index_range[1]) + if index0 >= 0 and index0 <= index1 and index1 < len(list_of_files): + valid_indices.extend(range(index0, index1 + 1)) + + return MapsToRun(list_of_files, valid_indices) + +def process(settings, verb, *, modify=True, backup=None): + if backup is None: + backup = modify # by default, backup when we modify + assert modify or not backup # doesn't make sense to backup when not modifying + + if len(sys.argv) > 1: + maps = sys.argv[1:] + else: + maps = prompt_maps(settings, verb) + maps = [str(maps.files[i]) for i in maps.indices] + print() + + if not maps: + print("No maps selected.") + return + + if modify: + print(f"Maps WILL{'' if settings.tgm else ' NOT'} be converted to tgm.") + if backup: + print("Backups will be created with a \".before\" extension.") + else: + print("Warning: backups are NOT being taken.") + + print(f"\nWill {verb} these maps:") + for path_str in maps: + print(pretty_path(settings, path_str)) + + try: + confirm = input(f"\nPress Enter to {verb}...\n") + except KeyboardInterrupt: + confirm = "^C" + if confirm != "": + print(f"\nAborted.") + return + + for path_str in maps: + print(f' - {pretty_path(settings, path_str)}') + + if backup: + shutil.copyfile(path_str, path_str + ".before") + + try: + yield path_str + except Exception as e: + print(f"Error: {e}") + else: + print("Succeeded.") + + print("\nFinished.") diff --git a/tools/mapmerge2/hooks/README.md b/tools/mapmerge2/hooks/README.md new file mode 100644 index 00000000000..844f3a3952f --- /dev/null +++ b/tools/mapmerge2/hooks/README.md @@ -0,0 +1,36 @@ +# Git Integration Hooks + +This folder contains installable scripts for [Git hooks] and [merge drivers]. +Use of these hooks and drivers is optional and they must be installed +explicitly before they take effect. + +To install the current set of hooks, or update if new hooks are added, run +`install.bat` (Windows) or `install.sh` (Unix-like) as appropriate. + +Hooks expect a Unix-like environment on the backend. Usually this is handled +automatically by GUI tools like TortoiseGit and GitHub for Windows, but +[Git for Windows] is an option if you prefer to use a CLI even on Windows. + +## Current Hooks + +* **Pre-commit**: Runs [mapmerge2] on changed maps, if any. + +## Adding New Hooks + +New [Git hooks] may be added by creating a file named `.hook` in +this directory. Git determines what hooks are available and what their names +are. The install script copies the `.hook` file into `.git/hooks`, so editing +the `.hook` file will require a reinstall. + +New [merge drivers] may be added by adding a shell script named `.merge` +and updating `.gitattributes` in the root of the repository to include the line +`*. merge=`. The install script will set up the merge driver to point +to the `.merge` file directly, and editing it will not require a reinstall. + +`tools/hooks/python.sh` may be used as a trampoline to ensure that the correct +version of Python is found. + +[Git hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks +[merge drivers]: https://git-scm.com/docs/gitattributes#_performing_a_three_way_merge +[Git for Windows]: https://gitforwindows.org/ +[mapmerge2]: ../mapmerge2/README.md diff --git a/tools/mapmerge2/hooks/install.bat b/tools/mapmerge2/hooks/install.bat new file mode 100644 index 00000000000..50fa647a667 --- /dev/null +++ b/tools/mapmerge2/hooks/install.bat @@ -0,0 +1,15 @@ +@echo off +cd %~dp0 +for %%f in (*.hook) do ( + echo Installing hook: %%~nf + mkdir ..\..\..\.git\hooks + copy %%f ..\..\..\.git\hooks\%%~nf >nul +) +for %%f in (*.merge) do ( + echo Installing merge driver: %%~nf + echo [merge "%%~nf"]^ + + driver = tools/hooks/%%f %%P %%O %%A %%B %%L >> ..\..\..\.git\config +) +echo Done +pause diff --git a/tools/mapmerge2/hooks/install.sh b/tools/mapmerge2/hooks/install.sh new file mode 100644 index 00000000000..be42d2ee964 --- /dev/null +++ b/tools/mapmerge2/hooks/install.sh @@ -0,0 +1,11 @@ +#!/bin/bash +cd "$(dirname "$0")" +for f in *.hook; do + echo Installing hook: ${f%.hook} + cp $f ../../../.git/hooks/${f%.hook} +done +for f in *.merge; do + echo Installing merge driver: ${f%.merge} + git config --replace-all merge.${f%.merge}.driver "tools/mapmerge2/hooks/$f %P %O %A %B %L" +done +echo "Done" diff --git a/tools/mapmerge2/hooks/pre-commit.hook b/tools/mapmerge2/hooks/pre-commit.hook new file mode 100644 index 00000000000..b78780ccbb9 --- /dev/null +++ b/tools/mapmerge2/hooks/pre-commit.hook @@ -0,0 +1,2 @@ +#!/bin/bash +exec tools/mapmerge2/hooks/python.sh -m precommit diff --git a/tools/mapmerge2/hooks/python.sh b/tools/mapmerge2/hooks/python.sh new file mode 100644 index 00000000000..32557070f48 --- /dev/null +++ b/tools/mapmerge2/hooks/python.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e +if command -v python3 >/dev/null 2>&1; then + PY=python3 +else + PY=python +fi +PATHSEP=$($PY - <<'EOF' +import sys, os +if sys.version_info.major != 3 or sys.version_info.minor < 6: + sys.stderr.write("Python 3.6+ is required: " + sys.version + "\n") + exit(1) +print(os.pathsep) +EOF +) +export PYTHONPATH=tools/mapmerge2/${PATHSEP}${PYTHONPATH} +$PY "$@" diff --git a/tools/mapmerge2/mapmerge.bat b/tools/mapmerge2/mapmerge.bat new file mode 100644 index 00000000000..06a689143ae --- /dev/null +++ b/tools/mapmerge2/mapmerge.bat @@ -0,0 +1,5 @@ +@echo off +set MAPROOT=../../maps/tether +set TGM=0 +python mapmerge.py +pause diff --git a/tools/mapmerge2/mapmerge.py b/tools/mapmerge2/mapmerge.py new file mode 100644 index 00000000000..f449bd948f0 --- /dev/null +++ b/tools/mapmerge2/mapmerge.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +import frontend +import shutil +from dmm import * +from collections import defaultdict + +def merge_map(new_map, old_map, delete_unused=False): + if new_map.key_length != old_map.key_length: + print("Warning: Key lengths differ, taking new map") + print(f" Old: {old_map.key_length}") + print(f" New: {new_map.key_length}") + return new_map + + if new_map.size != old_map.size: + print("Warning: Map dimensions differ, taking new map") + print(f" Old: {old_map.size}") + print(f" New: {new_map.size}") + return new_map + + key_length, size = old_map.key_length, old_map.size + merged = DMM(key_length, size) + merged.dictionary = old_map.dictionary.copy() + + known_keys = dict() # mapping fron 'new' key to 'merged' key + unused_keys = set(old_map.dictionary.keys()) # keys going unused + + # step one: parse the new version, compare it to the old version, merge both + for z, y, x in new_map.coords_zyx: + new_key = new_map.grid[x, y, z] + # if this key has been processed before, it can immediately be merged + try: + merged.grid[x, y, z] = known_keys[new_key] + continue + except KeyError: + pass + + def select_key(assigned): + merged.grid[x, y, z] = known_keys[new_key] = assigned + + old_key = old_map.grid[x, y, z] + old_tile = old_map.dictionary[old_key] + new_tile = new_map.dictionary[new_key] + + # this tile is the exact same as before, so the old key is used + if new_tile == old_tile: + select_key(old_key) + unused_keys.remove(old_key) + + # the tile is different here, but if it exists in the merged dictionary, that key can be used + elif new_tile in merged.dictionary.inv: + newold_key = merged.dictionary.inv[new_tile] + select_key(newold_key) + unused_keys.remove(newold_key) + + # the tile is brand new and it needs a new key, but if the old key isn't being used any longer it can be used instead + elif old_tile not in new_map.dictionary.inv and old_key in unused_keys: + merged.dictionary[old_key] = new_tile + select_key(old_key) + unused_keys.remove(old_key) + + # all other options ruled out, a brand new key is generated for the brand new tile + else: + fresh_key = merged.generate_new_key() + merged.dictionary[fresh_key] = new_tile + select_key(fresh_key) + + # step two: delete unused keys + if unused_keys: + print(f"Notice: Trimming {len(unused_keys)} unused dictionary keys.") + for key in unused_keys: + del merged.dictionary[key] + + # sanity check: that the merged map equals the new map + for z, y, x in new_map.coords_zyx: + new_tile = new_map.dictionary[new_map.grid[x, y, z]] + merged_tile = merged.dictionary[merged.grid[x, y, z]] + if new_tile != merged_tile: + print(f"Error: the map has been mangled! This is a mapmerge bug!") + print(f"At {x},{y},{z}.") + print(f"Should be {new_tile}") + print(f"Instead is {merged_tile}") + raise RuntimeError() + + return merged + +def main(settings): + for fname in frontend.process(settings, "merge", backup=True): + shutil.copyfile(fname, fname + ".before") + old_map = DMM.from_file(fname + ".backup") + new_map = DMM.from_file(fname) + merge_map(new_map, old_map).to_file(fname, settings.tgm) + +if __name__ == '__main__': + main(frontend.read_settings()) diff --git a/tools/mapmerge2/precommit.py b/tools/mapmerge2/precommit.py new file mode 100644 index 00000000000..381f1ea8a3e --- /dev/null +++ b/tools/mapmerge2/precommit.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +import os +import pygit2 +import dmm +from mapmerge import merge_map + +def main(repo): + if repo.index.conflicts: + print("You need to resolve merge conflicts first.") + return 1 + + changed = 0 + for path, status in repo.status().items(): + if path.endswith(".dmm") and (status & (pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_INDEX_NEW)): + # read the index + index_entry = repo.index[path] + index_map = dmm.DMM.from_bytes(repo[index_entry.id].read_raw()) + + try: + head_blob = repo[repo[repo.head.target].tree[path].id] + except KeyError: + # New map, no entry in HEAD + print(f"Converting new map: {path}") + assert (status & pygit2.GIT_STATUS_INDEX_NEW) + merged_map = index_map + else: + # Entry in HEAD, merge the index over it + print(f"Merging map: {path}") + assert not (status & pygit2.GIT_STATUS_INDEX_NEW) + head_map = dmm.DMM.from_bytes(head_blob.read_raw()) + merged_map = merge_map(index_map, head_map) + + # write to the index + blob_id = repo.create_blob(merged_map.to_bytes()) + repo.index.add(pygit2.IndexEntry(path, blob_id, index_entry.mode)) + changed += 1 + + # write to the working directory if that's clean + if status & (pygit2.GIT_STATUS_WT_DELETED | pygit2.GIT_STATUS_WT_MODIFIED): + print(f"Warning: {path} has unindexed changes, not overwriting them") + else: + merged_map.to_file(os.path.join(repo.workdir, path)) + + if changed: + repo.index.write() + print(f"Merged {changed} maps.") + return 0 + +if __name__ == '__main__': + exit(main(pygit2.Repository(pygit2.discover_repository(os.getcwd())))) diff --git a/tools/mapmerge2/requirements-install.bat b/tools/mapmerge2/requirements-install.bat new file mode 100644 index 00000000000..71341af8792 --- /dev/null +++ b/tools/mapmerge2/requirements-install.bat @@ -0,0 +1,3 @@ +@echo off +python -m pip install -r requirements.txt +pause diff --git a/tools/mapmerge2/requirements.txt b/tools/mapmerge2/requirements.txt new file mode 100644 index 00000000000..d01a2c6ccff --- /dev/null +++ b/tools/mapmerge2/requirements.txt @@ -0,0 +1,2 @@ +pygit2==0.26.0 +bidict==0.13.1 diff --git a/tools/mapmerge2/tgm2dmm.bat b/tools/mapmerge2/tgm2dmm.bat new file mode 100644 index 00000000000..2748533febf --- /dev/null +++ b/tools/mapmerge2/tgm2dmm.bat @@ -0,0 +1,5 @@ +@echo off +set MAPROOT=../../_maps/ +set TGM=0 +python convert.py +pause diff --git a/tools/mapmergepy/1 Prepare Maps.bat b/tools/mapmergepy/1 Prepare Maps.bat deleted file mode 100644 index d878510e3b3..00000000000 --- a/tools/mapmergepy/1 Prepare Maps.bat +++ /dev/null @@ -1,12 +0,0 @@ -@echo off -cd ../../maps/ - -for /R %%f in (*.dmm) do copy "%%f" "%%f.backup" - -cls -echo All dmm files in maps directories have been backed up -echo Now you can make your changes... -echo --- -echo Remember to run Run_Map_Merge.bat just before you commit your changes! -echo --- -pause diff --git a/tools/mapmergepy/2 Clean Maps.bat b/tools/mapmergepy/2 Clean Maps.bat deleted file mode 100644 index b651f5da712..00000000000 --- a/tools/mapmergepy/2 Clean Maps.bat +++ /dev/null @@ -1,4 +0,0 @@ -@echo off -set MAPROOT="../../maps/" -python mapmerger.py %1 %MAPROOT% -pause \ No newline at end of file diff --git a/tools/mapmergepy/__pycache__/map_helpers.cpython-36.pyc b/tools/mapmergepy/__pycache__/map_helpers.cpython-36.pyc deleted file mode 100644 index 3bee5f17afa8c4d4a7ea784c168c99d81e428209..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7998 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 1: - i = 1 - nextnew = key - while i < difference: - nextnew = get_next_key(nextnew) - unused_keys.append(nextnew) - i += 1 - sort = 1 - - - #Recycle outdated keys with any new tile data, starting from the bottom of the dictionary - i = 0 - for key, value in reversed(tempDict.items()): - recycled_key = key - if len(unused_keys) > 0: - recycled_key = unused_keys.pop() - - for coord, gridkey in tempGrid.items(): - if gridkey == None: - continue - if gridkey == key: - mergeGrid[coord] = recycled_key - tempGrid[coord] = None - - originalDict[recycled_key] = value - - #if gaps in the key sequence were found, sort the dictionary for cleanliness - if sort == 1: - sorted_dict = collections.OrderedDict() - next_key = get_next_key("") - while len(sorted_dict) < len(originalDict): - try: - sorted_dict[next_key] = originalDict[next_key] - except KeyError: - pass - next_key = get_next_key(next_key) - originalDict = sorted_dict - - if tgm: - with open(newfile, "w") as output: - write_dictionary_tgm(output, originalDict) - write_grid_coord_small(output, mergeGrid) - else: - with open(newfile, "wt", encoding='cp1252', newline='\n') as output: - write_dictionary_dmm(output, originalDict) - write_grid_dmm(output, mergeGrid) - return 1 - -#write dictionary in tgm format -def write_dictionary_tgm(output, dictionary): - output.write("//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE \n") - for key, list_ in dictionary.items(): - output.write("\"{}\" = (\n".format(key)) - - for thing in list_: - buffer = "" - in_quote_block = False - in_varedit_block = False - for char in thing: - - if in_quote_block: - if char == "\"": - in_quote_block = False - buffer = buffer + char - continue - elif char == "\"": - in_quote_block = True - buffer = buffer + char - continue - - if not in_varedit_block: - if char == "{": - in_varedit_block = True - buffer = buffer + "{\n\t" - continue - else: - if char == ";": - buffer = buffer + ";\n\t" - continue - elif char == "}": - buffer = buffer + "\n\t}" - in_varedit_block = False - continue - - buffer = buffer + char - - if list_.index(thing) != len(list_) - 1: - buffer = buffer + ",\n" - output.write(buffer) - - output.write(")\n") - -#thanks to YotaXP for finding out about this one -def write_grid_coord_small(output, grid): - output.write("\n") - - for x in range(1, maxx+1): - output.write("({},{},1) = {{\"\n".format(x, 1, 1)) - for y in range(1, maxy): - output.write("{}\n".format(grid[x,y])) - output.write("{}\n\"}}\n".format(grid[x,maxy])) - -def search_key(dictionary, data): - for key, value in dictionary.items(): - if value == data: - return key - return None - -def generate_new_key(dictionary): - last_key = next(reversed(dictionary)) - return get_next_key(last_key) - -def get_next_key(key): - if key == "": - return "".join("a" for _ in range(key_length)) - - length = len(key) - new_key = "" - carry = 1 - for char in key[::-1]: - if carry <= 0: - new_key = new_key + char - continue - if char == 'Z': - new_key = new_key + 'a' - carry += 1 - length -= 1 - if length <= 0: - return "OVERFLOW" - elif char == 'z': - new_key = new_key + 'A' - else: - new_key = new_key + chr(ord(char) + 1) - if carry > 0: - carry -= 1 - return new_key[::-1] - -#still does not support more than one z level per file, but should parse any format -def parse_map(map_file): - with open(map_file, "r") as map_input: - characters = map_input.read() - - in_quote_block = False - in_key_block = False - in_data_block = False - in_varedit_block = False - after_data_block = False - escaping = False - skip_whitespace = False - - dictionary = collections.OrderedDict() - curr_key = "" - curr_datum = "" - curr_data = list() - - in_map_block = False - in_coord_block = False - in_map_string = False - iter_x = 0 - adjust_y = True - - curr_num = "" - reading_coord = "x" - - global key_length - global maxx - global maxy - curr_x = 0 - curr_y = 0 - curr_z = 1 - grid = dict() - - for char in characters: - - if not in_map_block: - - if char == "\n" or char == "\t": - continue - - if in_data_block: - - if in_varedit_block: - - if in_quote_block: - if char == "\\": - curr_datum = curr_datum + char - escaping = True - continue - - if escaping: - curr_datum = curr_datum + char - escaping = False - continue - - if char == "\"": - curr_datum = curr_datum + char - in_quote_block = False - continue - - curr_datum = curr_datum + char - continue - - if skip_whitespace and char == " ": - skip_whitespace = False - continue - skip_whitespace = False - - if char == "\"": - curr_datum = curr_datum + char - in_quote_block = True - continue - - if char == ";": - skip_whitespace = False - curr_datum = curr_datum + char - continue - - if char == "}": - curr_datum = curr_datum + char - in_varedit_block = False - continue - - curr_datum = curr_datum + char - continue - - if char == "{": - curr_datum = curr_datum + char - in_varedit_block = True - continue - - if char == ",": - curr_data.append(curr_datum) - curr_datum = "" - continue - - if char == ")": - curr_data.append(curr_datum) - dictionary[curr_key] = tuple(curr_data) - curr_data = list() - curr_datum = "" - curr_key = "" - in_data_block = False - after_data_block = True - continue - - curr_datum = curr_datum + char - continue - - if in_key_block: - if char == "\"": - in_key_block = False - key_length = len(curr_key) - else: - curr_key = curr_key + char - continue - #else we're looking for a key block, a data block or the map block - - if char == "\"": - in_key_block = True - after_data_block = False - continue - - if char == "(": - if after_data_block: - in_map_block = True - in_coord_block = True - after_data_block = False - curr_key = "" - continue - else: - in_data_block = True - after_data_block = False - continue - - else: - - if in_coord_block: - if char == ",": - if reading_coord == "x": - curr_x = string_to_num(curr_num) - if curr_x > maxx: - maxx = curr_x - iter_x = 0 - curr_num = "" - reading_coord = "y" - elif reading_coord == "y": - curr_y = string_to_num(curr_num) - if curr_y > maxy: - maxy = curr_y - curr_num = "" - reading_coord = "z" - else: - pass - continue - - if char == ")": - in_coord_block = False - reading_coord = "x" - curr_num = "" - #read z here if needed - continue - - curr_num = curr_num + char - continue - - if in_map_string: - - if char == "\"": - in_map_string = False - adjust_y = True - curr_y -= 1 - continue - - if char == "\n": - if adjust_y: - adjust_y = False - else: - curr_y += 1 - if curr_x > maxx: - maxx = curr_x - if iter_x > 1: - curr_x = 1 - iter_x = 0 - continue - - - curr_key = curr_key + char - if len(curr_key) == key_length: - iter_x += 1 - if iter_x > 1: - curr_x += 1 - - grid[curr_x, curr_y] = curr_key - curr_key = "" - continue - - - #else look for coordinate block or a map string - - if char == "(": - in_coord_block = True - continue - if char == "\"": - in_map_string = True - continue - - if curr_y > maxy: - maxy = curr_y - - data = dict() - data["dictionary"] = dictionary - data["grid"] = grid - return data - -#subtract keyB from keyA -def key_difference(keyA, keyB): - if len(keyA) != len(keyB): - return "you fucked up" - - Ayek = keyA[::-1] - Byek = keyB[::-1] - - result = 0 - for i in range(0, len(keyA)): - base = 52**i - A = 26 if Ayek[i].isupper() else 0 - B = 26 if Byek[i].isupper() else 0 - result += ( (ord(Byek[i].lower()) + B) - (ord(Ayek[i].lower()) + A) ) * base - return result - -def string_to_num(s): - try: - return int(s) - except ValueError: - return -1 - -#writes a tile data dictionary the same way Dreammaker does -def write_dictionary_dmm(output, dictionary): - for key, value in dictionary.items(): - output.write("\"{}\" = ({})\n".format(key, ",".join(value))) - -#writes a map grid the same way Dreammaker does -def write_grid_dmm(output, grid): - output.write("\n") - output.write("(1,1,1) = {\"\n") - - for y in range(1, maxy+1): - for x in range(1, maxx+1): - try: - output.write(grid[x,y]) - except KeyError: - print("Key error: ({},{})".format(x,y)) - output.write("\n") - output.write("\"}") - output.write("\n") - -#inflated map grid; unused -def write_grid_coord(filename, grid): - with open(filename, "a") as output: - output.write("\n") - for y in range(1, maxy+1): - for x in range(1, maxx+1): - output.write("({},{},1) = {{\"{}\"}}\n".format(x, y, grid[x,y])) - -def key_compare(keyA, keyB): #thanks byond for not respecting ascii - pos = 0 - for a in keyA: - pos += 1 - count = pos - for b in keyB: - if(count > 1): - count -= 1 - continue - if a.islower() and b.islower(): - if(a < b): - return -1 - if(a > b): - return 1 - break - if a.islower() and b.isupper(): - return -1 - if a.isupper() and b.islower(): - return 1 - if a.isupper() and b.isupper(): - if(a < b): - return -1 - if(a > b): - return 1 - break - return 0 diff --git a/tools/mapmergepy/mapmerger.py b/tools/mapmergepy/mapmerger.py deleted file mode 100644 index 0a177eb07ac..00000000000 --- a/tools/mapmergepy/mapmerger.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/python3 - -import sys -import os -import pathlib -import map_helpers -import shutil - -def main(map_folder): - list_of_files = list() - for root, directories, filenames in os.walk(map_folder): - for filename in [f for f in filenames if f.endswith(".dmm")]: - list_of_files.append(pathlib.Path(root, filename)) - - last_dir = "" - for i in range(0, len(list_of_files)): - this_dir = list_of_files[i].parent - if last_dir != this_dir: - print("--------------------------------") - last_dir = this_dir - print("[{}]: {}".format(i, str(list_of_files[i])[len(map_folder):])) - - print("--------------------------------") - in_list = input("List the maps you want to merge (example: 1,3-5,12):\n") - in_list = in_list.replace(" ", "") - in_list = in_list.split(",") - - valid_indices = list() - for m in in_list: - index_range = m.split("-") - if len(index_range) == 1: - index = string_to_num(index_range[0]) - if index >= 0 and index < len(list_of_files): - valid_indices.append(index) - elif len(index_range) == 2: - index0 = string_to_num(index_range[0]) - index1 = string_to_num(index_range[1]) - if index0 >= 0 and index0 <= index1 and index1 < len(list_of_files): - valid_indices.extend(range(index0, index1 + 1)) - -# if tgm == "1": -# print("\nMaps will be converted to tgm.") -# tgm = True -# else: -# print("\nMaps will not be converted to tgm.") -# tgm = False - tgm = False - - print("\nMerging these maps:") - for i in valid_indices: - print(str(list_of_files[i])[len(map_folder):]) - merge = input("\nPress Enter to merge...") - if merge == "abort": - print("\nAborted map merge.") - sys.exit() - else: - for i in valid_indices: - path_str = str(list_of_files[i]) - shutil.copyfile(path_str, path_str + ".before") - path_str_pretty = path_str[len(map_folder):] - try: - if map_helpers.merge_map(path_str, path_str + ".backup", tgm) != 1: - print("ERROR MERGING: {}".format(path_str_pretty)) - os.remove(path_str + ".before") - continue - print("MERGED: {}".format(path_str_pretty)) - except FileNotFoundError: - print("\nERROR: File not found! Make sure you run 'Prepare Maps.bat' before merging.") - print(path_str_pretty + " || " + path_str_pretty + ".backup") - - print("\nFinished merging.") - print("\nNOTICE: A version of the map files from before merging have been created for debug purposes.\nDo not delete these files until it is sure your map edits have no undesirable changes.") - -def string_to_num(s): - try: - return int(s) - except ValueError: - return -1 - -if __name__ == "__main__": - main(sys.argv[1]) diff --git a/tools/mapmergepy/prepare_maps.sh b/tools/mapmergepy/prepare_maps.sh deleted file mode 100644 index f1ea86ce450..00000000000 --- a/tools/mapmergepy/prepare_maps.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -find ../../maps | grep \.dmm$ | xargs -l1 -I{} cp {} {}.backup \ No newline at end of file