mirror of
https://github.com/CHOMPStation2/CHOMPStation2.git
synced 2025-12-11 18:53:06 +00:00
Add MapMerge2 from /tg/
This commit is contained in:
52
tools/mapmerge2/README.md
Normal file
52
tools/mapmerge2/README.md
Normal file
@@ -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
|
||||||
52
tools/mapmerge2/README.txt
Normal file
52
tools/mapmerge2/README.txt
Normal file
@@ -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
|
||||||
8
tools/mapmerge2/convert.py
Normal file
8
tools/mapmerge2/convert.py
Normal file
@@ -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)
|
||||||
459
tools/mapmerge2/dmm.py
Normal file
459
tools/mapmerge2/dmm.py
Normal file
@@ -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
|
||||||
5
tools/mapmerge2/dmm2tgm.bat
Normal file
5
tools/mapmerge2/dmm2tgm.bat
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
set MAPROOT=../../_maps/
|
||||||
|
set TGM=1
|
||||||
|
python convert.py
|
||||||
|
pause
|
||||||
127
tools/mapmerge2/frontend.py
Normal file
127
tools/mapmerge2/frontend.py
Normal file
@@ -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.")
|
||||||
36
tools/mapmerge2/hooks/README.md
Normal file
36
tools/mapmerge2/hooks/README.md
Normal file
@@ -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-name>.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 `<ext>.merge`
|
||||||
|
and updating `.gitattributes` in the root of the repository to include the line
|
||||||
|
`*.<ext> merge=<ext>`. 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
|
||||||
15
tools/mapmerge2/hooks/install.bat
Normal file
15
tools/mapmerge2/hooks/install.bat
Normal file
@@ -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
|
||||||
11
tools/mapmerge2/hooks/install.sh
Normal file
11
tools/mapmerge2/hooks/install.sh
Normal file
@@ -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"
|
||||||
2
tools/mapmerge2/hooks/pre-commit.hook
Normal file
2
tools/mapmerge2/hooks/pre-commit.hook
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
exec tools/mapmerge2/hooks/python.sh -m precommit
|
||||||
17
tools/mapmerge2/hooks/python.sh
Normal file
17
tools/mapmerge2/hooks/python.sh
Normal file
@@ -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 "$@"
|
||||||
5
tools/mapmerge2/mapmerge.bat
Normal file
5
tools/mapmerge2/mapmerge.bat
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
set MAPROOT=../../maps/tether
|
||||||
|
set TGM=0
|
||||||
|
python mapmerge.py
|
||||||
|
pause
|
||||||
94
tools/mapmerge2/mapmerge.py
Normal file
94
tools/mapmerge2/mapmerge.py
Normal file
@@ -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())
|
||||||
50
tools/mapmerge2/precommit.py
Normal file
50
tools/mapmerge2/precommit.py
Normal file
@@ -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()))))
|
||||||
3
tools/mapmerge2/requirements-install.bat
Normal file
3
tools/mapmerge2/requirements-install.bat
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@echo off
|
||||||
|
python -m pip install -r requirements.txt
|
||||||
|
pause
|
||||||
2
tools/mapmerge2/requirements.txt
Normal file
2
tools/mapmerge2/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pygit2==0.26.0
|
||||||
|
bidict==0.13.1
|
||||||
5
tools/mapmerge2/tgm2dmm.bat
Normal file
5
tools/mapmerge2/tgm2dmm.bat
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
@echo off
|
||||||
|
set MAPROOT=../../_maps/
|
||||||
|
set TGM=0
|
||||||
|
python convert.py
|
||||||
|
pause
|
||||||
@@ -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
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
@echo off
|
|
||||||
set MAPROOT="../../maps/"
|
|
||||||
python mapmerger.py %1 %MAPROOT%
|
|
||||||
pause
|
|
||||||
Binary file not shown.
@@ -1,524 +0,0 @@
|
|||||||
import collections
|
|
||||||
|
|
||||||
maxx = 0
|
|
||||||
maxy = 0
|
|
||||||
key_length = 1
|
|
||||||
|
|
||||||
def reset_globals():
|
|
||||||
global key_length
|
|
||||||
global maxx
|
|
||||||
global maxy
|
|
||||||
key_length = 1
|
|
||||||
maxx = 0
|
|
||||||
maxy = 0
|
|
||||||
|
|
||||||
def merge_map(newfile, backupfile, tgm):
|
|
||||||
reset_globals()
|
|
||||||
|
|
||||||
shitmap = parse_map(newfile)
|
|
||||||
shitDict = shitmap["dictionary"] #key to tile data dictionary
|
|
||||||
shitGrid = shitmap["grid"] #x,y coords to tiles (keys) dictionary (the map's layout)
|
|
||||||
|
|
||||||
originalmap = parse_map(backupfile)
|
|
||||||
originalDict = originalmap["dictionary"]
|
|
||||||
originalGrid = originalmap["grid"]
|
|
||||||
|
|
||||||
mergeGrid = dict() #final map layout
|
|
||||||
known_keys = dict() #mapping known keys to original keys
|
|
||||||
tempGrid = dict() #saving tiles with newly generated keys for later processing
|
|
||||||
temp_keys = dict() #mapping known keys to newly generated keys
|
|
||||||
unused_keys = list(originalDict.keys()) #list with all existing keys that aren't being used
|
|
||||||
tempDict = collections.OrderedDict() #mapping new keys to new data
|
|
||||||
originalDict_size = len(originalDict)
|
|
||||||
|
|
||||||
for y in range(1,maxy+1):
|
|
||||||
for x in range(1,maxx+1):
|
|
||||||
shitKey = shitGrid[x,y]
|
|
||||||
|
|
||||||
#if this key was seen before, add it to the pile immediately
|
|
||||||
if shitKey in known_keys:
|
|
||||||
mergeGrid[x,y] = known_keys[shitKey]
|
|
||||||
continue
|
|
||||||
#if this key was seen before, add it to the pile immediately
|
|
||||||
if shitKey in temp_keys:
|
|
||||||
tempGrid[x,y] = temp_keys[shitKey]
|
|
||||||
continue
|
|
||||||
|
|
||||||
shitData = shitDict[shitKey]
|
|
||||||
originalKey = originalGrid[x,y]
|
|
||||||
originalData = originalDict[originalKey]
|
|
||||||
|
|
||||||
#if new tile data at x,y is the same as original tile data at x,y, add to the pile
|
|
||||||
if shitData == originalData:
|
|
||||||
mergeGrid[x,y] = originalKey
|
|
||||||
known_keys[shitKey] = originalKey
|
|
||||||
unused_keys.remove(originalKey)
|
|
||||||
else:
|
|
||||||
#search for the new tile data in the original dictionary, if a key is found add it to the pile, else generate a new key
|
|
||||||
newKey = search_key(originalDict, shitData)
|
|
||||||
if newKey != None:
|
|
||||||
try:
|
|
||||||
unused_keys.remove(newKey)
|
|
||||||
except ValueError: #caused by a duplicate entry
|
|
||||||
print("WARNING: Correcting duplicate dictionary entry. ({})".format(shitKey))
|
|
||||||
mergeGrid[x,y] = newKey
|
|
||||||
known_keys[shitKey] = newKey
|
|
||||||
#if data at original x,y no longer exists we reuse the key immediately
|
|
||||||
elif search_key(shitDict, originalData) == None:
|
|
||||||
mergeGrid[x,y] = originalKey
|
|
||||||
originalDict[originalKey] = shitData
|
|
||||||
unused_keys.remove(originalKey)
|
|
||||||
known_keys[shitKey] = originalKey
|
|
||||||
else:
|
|
||||||
if len(tempDict) == 0:
|
|
||||||
newKey = generate_new_key(originalDict)
|
|
||||||
else:
|
|
||||||
newKey = generate_new_key(tempDict)
|
|
||||||
if newKey == "OVERFLOW": #if this happens, merging is impossible
|
|
||||||
print("ERROR: Key overflow detected.")
|
|
||||||
return 0
|
|
||||||
tempGrid[x,y] = newKey
|
|
||||||
temp_keys[shitKey] = newKey
|
|
||||||
tempDict[newKey] = shitData
|
|
||||||
|
|
||||||
sort = 0
|
|
||||||
#find gaps in the dictionary keys sequence and add the missing keys to be recycled
|
|
||||||
dict_list = list(originalDict.keys())
|
|
||||||
for index in range(0, len(dict_list)):
|
|
||||||
if index + 1 == len(dict_list):
|
|
||||||
break
|
|
||||||
|
|
||||||
key = dict_list[index]
|
|
||||||
next_key = dict_list[index+1]
|
|
||||||
|
|
||||||
difference = key_difference(key, next_key)
|
|
||||||
if difference > 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
|
|
||||||
@@ -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])
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
find ../../maps | grep \.dmm$ | xargs -l1 -I{} cp {} {}.backup
|
|
||||||
Reference in New Issue
Block a user