diff --git a/.gitignore b/.gitignore
index 9a9bbe0aafd..861b69b8be0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@ data/
/_maps/map_files/**/*.dmm.backup
/_maps/quicksave/*
/nano/debug.html
+**/__pycache__/*
*.db
stddef.dm
.atom-build.json
diff --git a/code/modules/admin/verbs/mapping.dm b/code/modules/admin/verbs/mapping.dm
index bfc4f7a8887..5a53b0201b3 100644
--- a/code/modules/admin/verbs/mapping.dm
+++ b/code/modules/admin/verbs/mapping.dm
@@ -30,6 +30,12 @@ var/intercom_range_display_status = 0
src.pixel_x = -224
src.pixel_y = -224
+/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/code/modules/hydroponics/trays/tray_soil.dm b/code/modules/hydroponics/trays/tray_soil.dm
index d8c207c1c67..efaaeee887c 100644
--- a/code/modules/hydroponics/trays/tray_soil.dm
+++ b/code/modules/hydroponics/trays/tray_soil.dm
@@ -11,6 +11,20 @@
return
else if(istype(O,/obj/item/weapon/crowbar))
return
+ else if(istype(O, /obj/item/weapon/shovel))
+ if(seed)
+ to_chat(user, "You dig up and dispose of \the [seed.display_name].")
+ seed = null
+ dead = 0
+ sampled = 0
+ age = 0
+ lastproduce = 0
+ yield_mod = 0
+ mutation_mod = 0
+ check_health()
+ else
+ to_chat(user, "You clear up [src]!")
+ qdel(src)
else
..()
diff --git a/code/modules/projectiles/ammunition/ammo_casings.dm b/code/modules/projectiles/ammunition/ammo_casings.dm
index df34a279caf..eb4e42ff821 100644
--- a/code/modules/projectiles/ammunition/ammo_casings.dm
+++ b/code/modules/projectiles/ammunition/ammo_casings.dm
@@ -3,6 +3,12 @@
caliber = "357"
projectile_type = /obj/item/projectile/bullet
+/obj/item/ammo_casing/rubber9mm
+ desc = "A 9mm rubber bullet casing."
+ caliber = "9mm"
+ icon_state = "r-casing"
+ projectile_type = /obj/item/projectile/bullet/weakbullet4
+
/obj/item/ammo_casing/a762
desc = "A 7.62mm bullet casing."
icon_state = "762-casing"
@@ -20,6 +26,7 @@
/obj/item/ammo_casing/c38
desc = "A .38 bullet casing."
caliber = "38"
+ icon_state = "r-casing"
projectile_type = /obj/item/projectile/bullet/weakbullet2/rubber
/obj/item/ammo_casing/c10mm
@@ -76,6 +83,12 @@
caliber = "4.6x30mm"
projectile_type = /obj/item/projectile/bullet/incendiary/firebullet
+/obj/item/ammo_casing/rubber45
+ desc = "A .45 rubber bullet casing."
+ caliber = ".45"
+ icon_state = "r-casing"
+ projectile_type = /obj/item/projectile/bullet/weakbullet4
+
/obj/item/ammo_casing/c45
desc = "A .45 bullet casing."
caliber = ".45"
@@ -120,7 +133,7 @@
/obj/item/ammo_casing/shotgun/rubbershot
name = "rubber shot"
desc = "A shotgun casing filled with densely-packed rubber balls, used to incapacitate crowds from a distance."
- icon_state = "bshell"
+ icon_state = "cshell"
projectile_type = /obj/item/projectile/bullet/rpellet
pellets = 6
variance = 25
@@ -254,7 +267,7 @@
/obj/item/ammo_casing/shotgun/tranquilizer
name = "tranquilizer darts"
desc = "A tranquilizer round used to subdue individuals utilizing stimulants."
- icon_state = "cshell"
+ icon_state = "nshell"
projectile_type = /obj/item/projectile/bullet/dart/syringe/tranquilizer
materials = list(MAT_METAL=250)
diff --git a/code/modules/projectiles/ammunition/boxes.dm b/code/modules/projectiles/ammunition/boxes.dm
index 894b5ac25b1..865ddc9641b 100644
--- a/code/modules/projectiles/ammunition/boxes.dm
+++ b/code/modules/projectiles/ammunition/boxes.dm
@@ -35,6 +35,12 @@
ammo_type = /obj/item/ammo_casing/c45
max_ammo = 20
+/obj/item/ammo_box/rubber45
+ name = "ammo box (.45 rubber)"
+ icon_state = "45box-r"
+ ammo_type = /obj/item/ammo_casing/rubber45
+ max_ammo = 16
+
/obj/item/ammo_box/a40mm
name = "ammo box (40mm grenades)"
icon_state = "40mm"
@@ -52,14 +58,14 @@
/obj/item/ammo_box/n762
name = "ammo box (7.62x38mmR)"
- icon_state = "10mmbox"
+ icon_state = "riflebox"
origin_tech = "combat=2"
ammo_type = /obj/item/ammo_casing/n762
max_ammo = 14
/obj/item/ammo_box/shotgun
name = "Ammunition Box (slug)"
- icon_state = "9mmbox"
+ icon_state = "slugbox"
origin_tech = "combat=2"
ammo_type = /obj/item/ammo_casing/shotgun
max_ammo = 7
@@ -67,26 +73,30 @@
/obj/item/ammo_box/shotgun/buck
name = "Ammunition Box (buckshot)"
+ icon_state = "buckshotbox"
ammo_type = /obj/item/ammo_casing/shotgun/buckshot
/obj/item/ammo_box/shotgun/stun
name = "Ammunition Box (stun shells)"
+ icon_state = "stunbox"
ammo_type = /obj/item/ammo_casing/shotgun/stunslug
materials = list(MAT_METAL=1750)
/obj/item/ammo_box/shotgun/beanbag
name = "Ammunition Box (beanbag shells)"
+ icon_state = "beanbagbox"
ammo_type = /obj/item/ammo_casing/shotgun/beanbag
materials = list(MAT_METAL=1750)
/obj/item/ammo_box/shotgun/rubbershot
name = "Ammunition Box (rubbershot shells)"
+ icon_state = "rubbershotbox"
ammo_type = /obj/item/ammo_casing/shotgun/rubbershot
materials = list(MAT_METAL=28000)
/obj/item/ammo_box/shotgun/tranquilizer
name = "Ammunition Box (tranquilizer darts)"
- icon_state = "45box"
+ icon_state = "tranqbox"
ammo_type = /obj/item/ammo_casing/shotgun/tranquilizer
materials = list(MAT_METAL=1750)
diff --git a/code/modules/projectiles/ammunition/magazines.dm b/code/modules/projectiles/ammunition/magazines.dm
index 0f587c54bf4..aecd66b1ea5 100644
--- a/code/modules/projectiles/ammunition/magazines.dm
+++ b/code/modules/projectiles/ammunition/magazines.dm
@@ -209,6 +209,34 @@
caliber = ".45"
max_ammo = 8
multiple_sprites = 1
+/obj/item/ammo_box/magazine/m45/enforcer45
+ name = "handgun magazine (.45)"
+ icon_state = "enforcer"
+ ammo_type = /obj/item/ammo_casing/rubber45
+
+/obj/item/ammo_box/magazine/m45/enforcer45/update_icon()
+ ..()
+ overlays.Cut()
+
+ var/ammo = ammo_count()
+ if(ammo && is_rubber())
+ overlays += image('icons/obj/ammo.dmi', icon_state = "enforcer-r")
+
+/obj/item/ammo_box/magazine/m45/enforcer45/examine(mob/user, var/distance)
+ ..()
+ if(distance <= 2)
+ to_chat(user, "It seems to be loaded with [is_rubber() ? "rubber" : "lethal"] bullets.")//only can see the topmost one.
+
+/obj/item/ammo_box/magazine/m45/enforcer45/proc/is_rubber()//if the topmost bullet is a rubber one
+ var/ammo = ammo_count()
+ if(!ammo)
+ return 0
+ if(istype(contents[contents.len], /obj/item/ammo_casing/rubber45))
+ return 1
+ return 0
+
+ /obj/item/ammo_box/magazine/m45/enforcer45/lethal
+ ammo_type = /obj/item/ammo_casing/c45
/obj/item/ammo_box/magazine/wt550m9
name = "wt550 magazine (4.6x30mm)"
diff --git a/code/modules/projectiles/guns/projectile/pistol.dm b/code/modules/projectiles/guns/projectile/pistol.dm
index d1d65dcbfc0..5de627d7933 100644
--- a/code/modules/projectiles/guns/projectile/pistol.dm
+++ b/code/modules/projectiles/guns/projectile/pistol.dm
@@ -40,6 +40,14 @@
mag_type = /obj/item/ammo_box/magazine/m50
can_suppress = 0
+/obj/item/weapon/gun/projectile/automatic/pistol/enforcer45
+ name = "Enforcer .45"
+ desc = "A pistol of modern design."
+ icon_state = "enforcer"
+ force = 10
+ mag_type = /obj/item/ammo_box/magazine/m45/enforcer45
+ can_suppress = 0
+
/obj/item/weapon/gun/projectile/automatic/pistol/deagle/update_icon()
..()
icon_state = "[initial(icon_state)][magazine ? "" : "-e"]"
diff --git a/code/modules/projectiles/projectile/bullets.dm b/code/modules/projectiles/projectile/bullets.dm
index 9c3475d2141..31a09ecde37 100644
--- a/code/modules/projectiles/projectile/bullets.dm
+++ b/code/modules/projectiles/projectile/bullets.dm
@@ -42,6 +42,7 @@
damage = 5
weaken = 3
stamina = 60
+ icon_state = "bullet-r"
/obj/item/projectile/bullet/weakbullet2/rubber //detective's bullets that don't embed
embed = 0
@@ -50,6 +51,13 @@
/obj/item/projectile/bullet/weakbullet3
damage = 20
+/obj/item/projectile/bullet/weakbullet4
+ name = "rubber bullet"
+ damage = 5
+ stamina = 30
+ icon_state = "bullet-r"
+ embed = 0
+ sharp = 0
/obj/item/projectile/bullet/toxinbullet
damage = 15
@@ -135,6 +143,7 @@
name = "rubber pellet"
damage = 3
stamina = 25
+ icon_state = "bullet-r"
/obj/item/projectile/bullet/stunshot//taser slugs for shotguns, nothing special
name = "stunshot"
diff --git a/html/changelog.html b/html/changelog.html
index 0dee1cf6183..d5208203144 100644
--- a/html/changelog.html
+++ b/html/changelog.html
@@ -55,6 +55,18 @@
-->
+
23 January 2017
+
FalseIncarnate updated:
+
+ - Returns the ability to remove vacant soil with shovels and spades, and adds the ability to dig up plants in soil to remove them instantly.
+
+
Stratus updated:
+
+ - NT Enforcer .45 pistol.
+ - .45 and 9mm rubber bullets.
+ - New shotgun ammo boxes.
+
+
18 January 2017
AndriiYukhymchak updated:
diff --git a/html/changelogs/.all_changelog.yml b/html/changelogs/.all_changelog.yml
index c1ed6a62ad9..ff8963f47e2 100644
--- a/html/changelogs/.all_changelog.yml
+++ b/html/changelogs/.all_changelog.yml
@@ -3400,3 +3400,11 @@ DO NOT EDIT THIS FILE BY HAND! AUTOMATICALLY GENERATED BY ss13_genchangelog.py.
- tweak: Admin announcements are no longer sent to player in the lobby.
- tweak: Regular announcements can now only be heard if you the player is alive,
not deaf and in range of an enabled radio.
+2017-01-23:
+ FalseIncarnate:
+ - rscadd: Returns the ability to remove vacant soil with shovels and spades, and
+ adds the ability to dig up plants in soil to remove them instantly.
+ Stratus:
+ - rscadd: NT Enforcer .45 pistol.
+ - rscadd: .45 and 9mm rubber bullets.
+ - imageadd: New shotgun ammo boxes.
diff --git a/icons/mob/screen_gen.dmi b/icons/mob/screen_gen.dmi
index 54d39e581db..8ca8456ea39 100644
Binary files a/icons/mob/screen_gen.dmi and b/icons/mob/screen_gen.dmi differ
diff --git a/icons/obj/ammo.dmi b/icons/obj/ammo.dmi
index 07c0c1aedab..1265749d726 100644
Binary files a/icons/obj/ammo.dmi and b/icons/obj/ammo.dmi differ
diff --git a/icons/obj/projectiles.dmi b/icons/obj/projectiles.dmi
index afdddf46abc..316790998c5 100644
Binary files a/icons/obj/projectiles.dmi and b/icons/obj/projectiles.dmi differ
diff --git a/icons/obj/terminals.dmi b/icons/obj/terminals.dmi
index 16d8bbcae7a..45baaf3f7ba 100644
Binary files a/icons/obj/terminals.dmi and b/icons/obj/terminals.dmi differ
diff --git a/tools/map_conflict_fixer/README.md b/tools/map_conflict_fixer/README.md
new file mode 100644
index 00000000000..5af8d3aadf0
--- /dev/null
+++ b/tools/map_conflict_fixer/README.md
@@ -0,0 +1,29 @@
+#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.
diff --git a/tools/map_conflict_fixer/Run Map Conflict Fixer - DMM.bat b/tools/map_conflict_fixer/Run Map Conflict Fixer - DMM.bat
new file mode 100644
index 00000000000..d66102a3295
--- /dev/null
+++ b/tools/map_conflict_fixer/Run Map Conflict Fixer - DMM.bat
@@ -0,0 +1,4 @@
+@echo off
+SET RELATIVEROOT="../../"
+python map_conflict_fixer.py %1 %RELATIVEROOT% 0
+pause
diff --git a/tools/map_conflict_fixer/map_conflict_fixer.py b/tools/map_conflict_fixer/map_conflict_fixer.py
new file mode 100644
index 00000000000..5e64657ad89
--- /dev/null
+++ b/tools/map_conflict_fixer/map_conflict_fixer.py
@@ -0,0 +1,111 @@
+import map_helpers
+import sys
+import os
+import time
+
+# Credits to tg for this
+
+def main(relative_root, tgm=0):
+ 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/mapfix_marker]: ")
+ if not marker:
+ marker = "/obj/effect/debugging/mapfix_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, tgm):
+ print("{}: {}".format(ed, path))
+ print(".")
+
+output_tgm = None
+try:
+ output_tgm = int(sys.argv[2])
+except ValueError:
+ output_tgm = 0
+
+main(sys.argv[1], output_tgm)
diff --git a/tools/map_conflict_fixer/map_helpers.py b/tools/map_conflict_fixer/map_helpers.py
new file mode 100644
index 00000000000..45f27e8c92c
--- /dev/null
+++ b/tools/map_conflict_fixer/map_helpers.py
@@ -0,0 +1,687 @@
+import sys
+import subprocess
+
+tgm_header = "//MAP CONVERTED BY dmm2tgm.py THIS HEADER COMMENT PREVENTS RECONVERSION, DO NOT REMOVE"
+
+try:
+ version = sys.version_info
+ if version.major < 3 or (version.major == 3 and version.minor < 5):
+ print("ERROR: You are running an incompatible version of Python. The current minimum version required is [3.5].\nYour version: {}".format(sys.version))
+ sys.exit()
+except:
+ print("ERROR: Something went wrong, you might be running an incompatible version of Python. The current minimum version required is [3.5].\nYour version: {}".format(sys.version))
+ sys.exit()
+
+import collections
+
+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):
+ key_length = 1
+ maxx = 1
+ maxy = 1
+
+ new_map = parse_map(get_map_raw_text(newfile))
+ old_map = parse_map(get_map_raw_text(backupfile))
+
+ if new_map["key_length"] != old_map["key_length"]:
+ if tgm:
+ write_dictionary_tgm(newfile, new_map["dictionary"])
+ write_grid_coord_small(newfile, new_map["grid"])
+ return 1
+ else:
+ key_length = old_map["key_length"]
+
+ 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:
+ 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 to file helpers#
+def write_dictionary_tgm(filename, dictionary): #write dictionary in tgm format
+ with open(filename, "w") as output:
+ output.write("{}\n".format(tgm_header))
+ 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")
+
+
+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")
+
+ 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 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, len(last_key))
+
+def get_next_key(key, key_length):
+ 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]
+
+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
+ 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
+
+
+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, tgm): #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)
+ if tgm:
+ write_dictionary_tgm(path+".fixed.dmm", merged_dict)
+ write_grid_coord_small(path+".fixed.dmm", merged_grid, maxx, maxy)
+ else:
+ write_dictionary(path+".fixed.dmm", merged_dict)
+ write_grid(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