diff --git a/code/modules/admin/verbs/mapping.dm b/code/modules/admin/verbs/mapping.dm index 4d01a5b4d9f..09b7e3c79d9 100644 --- a/code/modules/admin/verbs/mapping.dm +++ b/code/modules/admin/verbs/mapping.dm @@ -45,6 +45,12 @@ var/list/admin_verbs_debug_mapping = list( /client/proc/manipulate_organs ) +/obj/effect/debugging/mapfix_marker + name = "map fix marker" + icon = 'icons/mob/screen_gen.dmi' + icon_state = "mapfixmarker" + desc = "I am a mappers mistake." + /obj/effect/debugging/marker icon = 'icons/turf/areas.dmi' icon_state = "yellow" diff --git a/icons/mob/screen_gen.dmi b/icons/mob/screen_gen.dmi index 256bd412b4f..40500e84a83 100644 Binary files a/icons/mob/screen_gen.dmi and b/icons/mob/screen_gen.dmi differ diff --git a/tools/mapmerge/README.md b/tools/mapmerge/README.md index d8d696d8939..3a9d0cb8cf5 100644 --- a/tools/mapmerge/README.md +++ b/tools/mapmerge/README.md @@ -35,3 +35,34 @@ Your map is now ready to be committed, rejoice and wait for conflicts. Open the map in dreameditor before committing the results of the mapmerger - this can cause dreameditor to resave the map back to dmm, if you're having issues with your map getting stuck in dmm mode, try comitting and pushing the mapmerger changes before reopening in dreameditor. + + +#Map Conflict Fixer/Helper# +The map conflict fixer is a script that can help you fix map conflicts easier and faster. Here's how it works: + +###Before using### +You need git for this, of course. +Make sure your development branch is up to date before starting a map edit to ensure the script outputs a correct fix. + +##Dictionary mode## +Dictionary conflicts are the easiest to fix, you simply need to create more models to accommodate your changes and everyone elses. + +When you run in this mode, if the script finishes successfuly the map should be ready to be commited. + +If the script fails in dictionary mode, you can run it again in full fix mode. + +##Full Fix mode## +When you and someone else edit the same coordinate, there is no easy way to fix the conflict. You need to get your hands dirty. + +The script will mark every tile with a marker type to help you identify what needs fixing in the map editor. + +After you edit and fix a marked map, you should run it through the map merger. The .backup file should be the same you used before. + +###Priorities### +In Full Fix mode, the script needs to know which map version has higher priority, yours or someone elses. This important so tiles with multiple area and turf types aren't created. + +Your version has priority - In each conflicted coordinate, your floor type and your area type will be used +Their version has priority - In each conflicted coordinate, your floor type and your area type will not be used + +##IMPORTANT## +This script is in a testing phase and you should not consider any output to be safe. Always verify the maps this script produced to make sure nothing is out of place. \ No newline at end of file diff --git a/tools/mapmerge/Run Map Conflict Fixer.bat b/tools/mapmerge/Run Map Conflict Fixer.bat new file mode 100644 index 00000000000..34eac701bbc --- /dev/null +++ b/tools/mapmerge/Run Map Conflict Fixer.bat @@ -0,0 +1,4 @@ +@echo off +SET RELATIVEROOT="../../" +python map_conflict_fixer.py %1 %RELATIVEROOT% +pause \ No newline at end of file diff --git a/tools/mapmerge/map_conflict_fixer.py b/tools/mapmerge/map_conflict_fixer.py new file mode 100644 index 00000000000..c3dc378bf1c --- /dev/null +++ b/tools/mapmerge/map_conflict_fixer.py @@ -0,0 +1,103 @@ +import map_helpers +import sys +import os +import time + +def main(relative_root): + git_version = map_helpers.run_shell_command("git version") + if not git_version: + print("ERROR: Failed to run git. Make sure it is installed and in your PATH.") + return False + + print("--- DISCLAIMER ---") + print("This script is in a testing phase. Verify all the results yourself to make sure you got what you expected. Make sure to read the readme to learn how to use this.") + input("Press Enter to GO\n") + + file_conflicts = map_helpers.run_shell_command("git diff --name-only --diff-filter=U").split("\n") + map_conflicts = [path for path in file_conflicts if path[len(path)-3::] == "dmm"] + + for i in range(0, len(map_conflicts)): + print("[{}]: {}".format(i, map_conflicts[i])) + selection = input("Choose maps you want to fix (example: 1,3-5,12):\n") + selection = selection.replace(" ", "") + selection = selection.split(",") + + #shamelessly copied from mapmerger cli + valid_indices = list() + for m in selection: + index_range = m.split("-") + if len(index_range) == 1: + index = map_helpers.string_to_num(index_range[0]) + if index >= 0 and index < len(map_conflicts): + valid_indices.append(index) + elif len(index_range) == 2: + index0 = map_helpers.string_to_num(index_range[0]) + index1 = map_helpers.string_to_num(index_range[1]) + if index0 >= 0 and index0 <= index1 and index1 < len(map_conflicts): + valid_indices.extend(range(index0, index1 + 1)) + + if not len(valid_indices): + print("No map selected, exiting.") + sys.exit() + + print("Attempting to fix the following maps:") + for i in valid_indices: + print(map_conflicts[i]) + + + marker = None + priority = 0 + print("\nFixing modes:") + print("[{}]: Dictionary conflict fixing mode".format(map_helpers.MAP_FIX_DICTIONARY)) + print("[{}]: Full map conflict fixing mode".format(map_helpers.MAP_FIX_FULL)) + mode = map_helpers.string_to_num(input("Select fixing mode [Dictionary]: ")) + if mode != map_helpers.MAP_FIX_FULL: + mode = map_helpers.MAP_FIX_DICTIONARY + print("DICTIONARY mode selected.") + else: + marker = input("FULL mode selected. Input a marker [/obj/effect/debugging/marker]: ") + if not marker: + marker = "/obj/effect/debugging/marker" + print("Marker selected: {}".format(marker)) + + print("\nVersion priorities:") + print("[{}]: Your version".format(map_helpers.MAP_FIX_PRIORITY_OURS)) + print("[{}]: Their version".format(map_helpers.MAP_FIX_PRIORITY_THEIRS)) + priority = map_helpers.string_to_num(input("Select priority [Yours]: ")) + if priority != map_helpers.MAP_FIX_PRIORITY_THEIRS: + priority = map_helpers.MAP_FIX_PRIORITY_OURS + print("Your version will be prioritized.") + else: + print("Their version will be prioritized.") + + ed = "FIXED" if mode == map_helpers.MAP_FIX_DICTIONARY else "MARKED" + ing = "FIXING" if mode == map_helpers.MAP_FIX_DICTIONARY else "MARKING" + + print("\nMaps will be converted to TGM.") + print("Writing maps to 'file_path/file_name.fixed.dmm'. Please verify the results before commiting.") + if mode == map_helpers.MAP_FIX_FULL: + print("After editing the marked maps, run them through the map merger!") + input("Press Enter to start.") + + print(".") + time.sleep(0.3) + print(".") + + for i in valid_indices: + path = map_conflicts[i] + print("{}: {}".format(ing, path)) + ours_map_raw_text = map_helpers.run_shell_command("git show HEAD:{}".format(path)) + theirs_map_raw_text = map_helpers.run_shell_command("git show MERGE_HEAD:{}".format(path)) + + common_ancestor_hash = map_helpers.run_shell_command("git merge-base HEAD MERGE_HEAD").strip() + base_map_raw_text = map_helpers.run_shell_command("git show {}:{}".format(common_ancestor_hash, path)) + + ours_map = map_helpers.parse_map(ours_map_raw_text) + theirs_map = map_helpers.parse_map(theirs_map_raw_text) + base_map = map_helpers.parse_map(base_map_raw_text) + + if map_helpers.fix_map_git_conflicts(base_map, ours_map, theirs_map, mode, marker, priority, relative_root+path): + print("{}: {}".format(ed, path)) + print(".") + +main(sys.argv[1]) diff --git a/tools/mapmerge/map_helpers.py b/tools/mapmerge/map_helpers.py index 5c399a12e5e..cb4849e9825 100644 --- a/tools/mapmerge/map_helpers.py +++ b/tools/mapmerge/map_helpers.py @@ -1,4 +1,7 @@ import sys +import subprocess + +tgm_header = "//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE" try: version = sys.version_info @@ -11,148 +14,110 @@ except: import collections -error = {0:"OK", 1:"WARNING: Key lengths are different, all the lines change."} - -maxx = 0 -maxy = 0 -key_length = 1 - -def reset_globals(): - global key_length - global maxx - global maxy - key_length = 1 - maxx = 0 - maxy = 0 +error = {0:"OK", 1:"WARNING: Detected key length difference, not merging.", 2:"WARNING: Detected map size difference, not merging."} def merge_map(newfile, backupfile, tgm): - reset_globals() + key_length = 1 + maxx = 1 + maxy = 1 - shitmap = parse_map(newfile) - originalmap = parse_map(backupfile) + new_map = parse_map(get_map_raw_text(newfile)) + old_map = parse_map(get_map_raw_text(backupfile)) - global key_length - if shitmap["key_length"] != originalmap["key_length"]: + if new_map["key_length"] != old_map["key_length"]: if tgm: - write_dictionary_tgm(newfile, shitmap["dictionary"]) - write_grid_coord_small(newfile, shitmap["grid"]) + write_dictionary_tgm(newfile, new_map["dictionary"]) + write_grid_coord_small(newfile, new_map["grid"]) return 1 else: - key_length = originalmap["key_length"] + key_length = old_map["key_length"] - shitDict = shitmap["dictionary"] #key to tile data dictionary - shitGrid = shitmap["grid"] #x,y coords to tiles (keys) dictionary (the map's layout) - - originalDict = sort_dictionary(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("NOTICE: 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) - 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: - originalDict = sort_dictionary(originalDict) - - if tgm: - write_dictionary_tgm(newfile, originalDict) - write_grid_coord_small(newfile, mergeGrid) + if new_map["maxx"] != old_map["maxx"] or new_map["maxy"] != old_map["maxy"]: + if tgm: + write_dictionary_tgm(newfile, new_map["dictionary"]) + write_grid_coord_small(newfile, new_map["grid"]) + return 2 else: - write_dictionary(newfile, originalDict) - write_grid(newfile, mergeGrid) + maxx = old_map["maxx"] + maxy = old_map["maxy"] + + new_dict = new_map["dictionary"] + new_grid = new_map["grid"] + old_dict = sort_dictionary(old_map["dictionary"]) #impose order; old_dict is used in the end as the merged dictionary + old_grid = old_map["grid"] + + merged_grid = dict() + known_keys = dict() + unused_keys = list(old_dict.keys()) + + #both new and old dictionary in lists, for faster key lookup by tile tuple + old_dict_keys = list(old_dict.keys()) + old_dict_values = list(old_dict.values()) + new_dict_keys = list(new_dict.keys()) + new_dict_values = list(new_dict.values()) + + #step one: parse the new version, compare it to the old version, merge both + for y in range(1, maxy+1): + for x in range(1, maxx+1): + + new_key = new_grid[x,y] + #if this key has been processed before, it can immediately be merged + if new_key in known_keys: + merged_grid[x,y] = known_keys[new_key] + continue + + old_key = old_grid[x,y] + old_tile = old_dict[old_key] + new_tile = new_dict[new_key] + + if new_tile == old_tile: #this tile is the exact same as before, so the old key is used + merged_grid[x,y] = old_key + known_keys[new_key] = old_key + unused_keys.remove(old_key) + continue + + #the tile is different here, but if it exists in the old dictionary, its old key can be used + newold_key = get_key(old_dict_keys, old_dict_values, new_tile) + if newold_key != None: + merged_grid[x,y] = newold_key + known_keys[new_key] = newold_key + try: + unused_keys.remove(newold_key) + except ValueError: + print("NOTICE: Correcting duplicate dictionary entry. ({})".format(new_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 get_key(new_dict_keys, new_dict_values, old_tile) == None: + merged_grid[x,y] = old_key + old_dict[old_key] = new_tile + known_keys[new_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 = generate_new_key(old_dict) + old_dict[fresh_key] = new_tile + merged_grid[x,y] = fresh_key + + #step two: clean the dictionary if it has too many unused keys + if len(unused_keys) > min(100, (len(old_dict) * 0.5)): + print("NOTICE: Trimming the dictionary.") + old_dict = trim_dictionary(old_dict) + + #step three: write the map to file + if tgm: + write_dictionary_tgm(newfile, old_dict) + write_grid_coord_small(newfile, merged_grid, maxx, maxy) + else: + write_dictionary(newfile, old_dict) + write_grid(newfile, merged_grid, maxx, maxy) return 0 -#write dictionary in tgm format -def write_dictionary_tgm(filename, dictionary): +####################### +#write to file helpers# +def write_dictionary_tgm(filename, dictionary): #write dictionary in tgm format with open(filename, "w") as output: - output.write("//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE \n") + output.write("{}\n".format(tgm_header)) for key, list_ in dictionary.items(): output.write("\"{}\" = (\n".format(key)) @@ -194,8 +159,8 @@ def write_dictionary_tgm(filename, dictionary): output.write(")\n") -#thanks to YotaXP for finding out about this one -def write_grid_coord_small(filename, grid): + +def write_grid_coord_small(filename, grid, maxx, maxy): #thanks to YotaXP for finding out about this one with open(filename, "a") as output: output.write("\n") @@ -205,17 +170,290 @@ def write_grid_coord_small(filename, grid): output.write("{}\n".format(grid[x,y])) output.write("{}\n\"}}\n".format(grid[x,maxy])) + +def write_dictionary(filename, dictionary): #writes a tile dictionary the same way Dreammaker does + with open(filename, "w") as output: + for key, value in dictionary.items(): + output.write("\"{}\" = ({})\n".format(key, ",".join(value))) + + +def write_grid(filename, grid, maxx, maxy): #writes a map grid the same way Dreammaker does + with open(filename, "a") as output: + 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") + +#################### +#dictionary helpers# def search_key(dictionary, data): for key, value in dictionary.items(): if value == data: return key return None +def get_key(keys, values, data): + try: + return keys[values.index(data)] + except: + return None + +def trim_dictionary(dictionary): #rewrites dictionary into an ordered dictionary with no unused keys + trimmed_dict = collections.OrderedDict() + key_length = len(list(dictionary.keys())[0]) + key = "" + for tile in dictionary.values(): + key = get_next_key(key, key_length) + trimmed_dict[key] = tile + return trimmed_dict + +def sort_dictionary(dictionary): + sorted_dict = collections.OrderedDict() + keys = list(dictionary.keys()) + keys.sort(key=key_int_value) + for key in keys: + sorted_dict[key] = dictionary[key] + return sorted_dict + +############ +#map parser# +def get_map_raw_text(mapfile): + with open(mapfile, "r") as reading: + return reading.read() + +def parse_map(map_raw_text): #still does not support more than one z level per file, but should parse any format + 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" + + key_length = 0 + + maxx = 0 + maxy = 0 + + curr_x = 0 + curr_y = 0 + curr_z = 1 + grid = dict() + + for char in map_raw_text: + + 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 = True + 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 + data["key_length"] = key_length + data["maxx"] = maxx + data["maxy"] = maxy + return data + +############# +#key helpers# def generate_new_key(dictionary): last_key = next(reversed(dictionary)) - return get_next_key(last_key) + return get_next_key(last_key, len(last_key)) -def get_next_key(key): +def get_next_key(key, key_length): if key == "": return "".join("a" for _ in range(key_length)) @@ -240,287 +478,13 @@ def get_next_key(key): carry -= 1 return new_key[::-1] -def sort_dictionary(dictionary): - sorted_dict = collections.OrderedDict() - next_key = get_next_key("") - while len(sorted_dict) < len(dictionary): - try: - sorted_dict[next_key] = dictionary[next_key] - except KeyError: - pass - next_key = get_next_key(next_key) - return sorted_dict - -#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 maxx - global maxy - key_length_local = 0 - 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 = True - 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_local = 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_local: - 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 - data["key_length"] = key_length_local - 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(filename, dictionary): - with open(filename, "w") as output: - 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(filename, grid): - with open(filename, "a") as output: - 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_int_value(key): + value = 0 + b = 0 + for digit in reversed(key): + value += base52.index(digit) * (52 ** b) + b += 1 + return value def key_compare(keyA, keyB): #thanks byond for not respecting ascii pos = 0 @@ -548,3 +512,172 @@ def key_compare(keyA, keyB): #thanks byond for not respecting ascii return 1 break return 0 + + +def key_difference(keyA, keyB): #subtract keyB from keyA + 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 + +############# +#other stuff# + +#Base 52 a-z A-Z dictionary (it's a python list) for fast conversion +base52 = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o', + 'p','q','r','s','t','u','v','w','x','y','z','A','B','C','D', + 'E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S', + 'T','U','V','W','X','Y','Z'] + +def string_to_num(s): + try: + return int(s) + except ValueError: + return -1 + +#################### +#map conflict fixer# + +MAP_FIX_DICTIONARY = 0 +MAP_FIX_FULL = 1 +MAP_FIX_PRIORITY_OURS = 0 +MAP_FIX_PRIORITY_THEIRS = 1 + +def fix_map_git_conflicts(base_map, ours_map, theirs_map, mode, marker, priority, path): #attempts to fix git map conflicts automagically + #mode values: + # 0 - Dictionary mode, only fixes dictionary conflicts and fails at coordinate conflicts + # 1 - Full mode, fixes dictionary conflicts and will join both versions of a coordinate and adds a marker to each conflicted coordinate + + if mode == MAP_FIX_FULL and not marker: + print("ERROR: Full fix mode selected but a marker wasn't provided.") + return False + + key_length = 1 + maxx = 1 + maxy = 1 + + if ours_map["key_length"] != theirs_map["key_length"] or ours_map["key_length"] != base_map["key_length"]: + print("ERROR: Key length is different, I am not smart enough for this yet.") + return False + else: + key_length = ours_map["key_length"] + + if ours_map["maxx"] != theirs_map["maxx"] or ours_map["maxy"] != theirs_map["maxy"] or ours_map["maxx"] != base_map["maxx"] or ours_map["maxy"] != base_map["maxy"]: + print("ERROR: Map size is different, I am not smart enough for this yet.") + return False + else: + maxx = ours_map["maxx"] + maxy = ours_map["maxy"] + + base_dict = base_map["dictionary"] + base_grid = base_map["grid"] + ours_dict = ours_map["dictionary"] + ours_grid = ours_map["grid"] + theirs_dict = theirs_map["dictionary"] + theirs_grid = theirs_map["grid"] + + merged_dict = collections.OrderedDict() + merged_grid = dict() + + new_key = generate_new_key(base_dict) + + grid_conflict_counter = 0 + + for y in range(1, maxy+1): + for x in range(1, maxx+1): + base_key = base_grid[x,y] + ours_key = ours_grid[x,y] + theirs_key = theirs_grid[x,y] + + #case 1: everything is the same + if base_dict[base_key] == ours_dict[ours_key] and base_dict[base_key] == theirs_dict[theirs_key]: + merged_dict[base_key] = base_dict[base_key] + merged_grid[x,y] = base_key + continue + + #case 2: ours is unchanged, theirs is different + if base_dict[base_key] == ours_dict[ours_key]: + some_key = search_key(base_dict, theirs_dict[theirs_key]) + if some_key != None: + merged_dict[some_key] = theirs_dict[theirs_key] + merged_grid[x,y] = some_key + else: + merged_dict[new_key] = theirs_dict[theirs_key] + merged_grid[x,y] = new_key + new_key = get_next_key(new_key, key_length) + + #case 3: ours is different, theirs is unchanged + elif base_dict[base_key] == theirs_dict[theirs_key]: + some_key = search_key(base_dict, ours_dict[ours_key]) + if some_key != None: + merged_dict[some_key] = ours_dict[ours_key] + merged_grid[x,y] = some_key + else: + merged_dict[new_key] = ours_dict[ours_key] + merged_grid[x,y] = new_key + new_key = get_next_key(new_key, key_length) + + #case 4: everything is different, grid conflict + else: + if mode != MAP_FIX_FULL: + print("FAILED: Map fixing failed due to grid conflicts. Try fixing in full fix mode.") + return False + else: + combined_tile = combine_tiles(ours_dict[ours_key], theirs_dict[theirs_key], priority, marker) + merged_dict[new_key] = combined_tile + merged_grid[x,y] = new_key + new_key = get_next_key(new_key, key_length) + grid_conflict_counter += 1 + + merged_dict = sort_dictionary(merged_dict) + write_dictionary_tgm(path+".fixed.dmm", merged_dict) + write_grid_coord_small(path+".fixed.dmm", merged_grid, maxx, maxy) + if grid_conflict_counter > 0: + print("Counted {} conflicts.".format(grid_conflict_counter)) + return True + +def combine_tiles(tile_A, tile_B, priority, marker): + new_tile = list() + turf_atom = None + area_atom = None + + low_tile = None + high_tile = None + if priority == MAP_FIX_PRIORITY_OURS: + high_tile = tile_A + low_tile = tile_B + else: + high_tile = tile_B + low_tile = tile_A + + new_tile.append(marker) + for atom in high_tile: + if atom[0:5] == "/turf": + turf_atom = atom + elif atom[0:5] == "/area": + area_atom = atom + else: + new_tile.append(atom) + + for atom in low_tile: + if atom[0:5] == "/turf" or atom[0:5] == "/area": + continue + else: + new_tile.append(atom) + + new_tile.append(turf_atom) + new_tile.append(area_atom) + return tuple(new_tile) + + +def run_shell_command(command): + return subprocess.run(command, shell=True, stdout=subprocess.PIPE, universal_newlines=True).stdout